From 527bd49c0f7c9dcdd1b2ddb58976525f0384ff54 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 29 Jan 2021 18:57:02 -0600 Subject: [PATCH 001/288] moved SimpleSparse and IdentityMatrix (and created new ZeroMatrix) to new utilities.special_matrices module --- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/jacobian.py | 287 +-------------- sequence_jacobian/utilities/__init__.py | 2 +- .../utilities/special_matrices.py | 339 ++++++++++++++++++ 4 files changed, 342 insertions(+), 288 deletions(-) create mode 100644 sequence_jacobian/utilities/special_matrices.py diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 138e516..2fe92ba 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -151,6 +151,6 @@ def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): J = {o: {} for o in utils.misc.output_list(f)} for o, o_name in zip(utils.misc.make_tuple(f(**input_args)), utils.misc.output_list(f)): if isinstance(o, AccumulatedDerivative): - J[o_name] = jacobian.SimpleSparse(o.elements) + J[o_name] = utils.special_matrices.SimpleSparse(o.elements) return J diff --git a/sequence_jacobian/jacobian.py b/sequence_jacobian/jacobian.py index 97e3af3..255dec5 100644 --- a/sequence_jacobian/jacobian.py +++ b/sequence_jacobian/jacobian.py @@ -311,7 +311,7 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): if jacflag: # Jacobians of inputs with respect to themselves are the identity, initialize with this - out = {i: {i: IdentityMatrix()} for i in inputs} + out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} else: out = inputs.copy() @@ -465,288 +465,3 @@ def make_ATI_v(x, tau): return x.v -'''Part 3: SimpleSparse and IdentityMatrix classes and related helpers''' -class SimpleSparse: - """Efficient representation of sparse linear operators, which are linear combinations of basis - operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s - (measured by # above main diagonal) and m is number of initial entries missing. - - Examples of such basis operators: - - (0, 0) is identity operator - - (0, 2) is identity operator with first two '1's on main diagonal missing - - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator - - (-1, 1) has 1s on diagonal below main diagonal, except first column - - The linear combination of these basis operators that makes up a given SimpleSparse object is - stored as a dict 'elements' mapping (i, m) -> x. - - The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need - the more general basis (i, m) to ensure closure under multiplication. - - These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space - Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation - for writing SimpleBlock functions. - - The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix - operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less - interchangeably with ordinary NumPy matrices. - """ - - # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules - __array_priority__ = 1000 - - def __init__(self, elements): - self.elements = elements - self.indices, self.xs = None, None - - @staticmethod - def from_simple_diagonals(elements): - """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" - return SimpleSparse({(i, 0): x for i, x in elements.items()}) - - def matrix(self, T): - """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" - return self + np.zeros((T, T)) - - def array(self): - """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) - and one size-N array of floats with entries x. - - This is needed for Numba to take as input. Cache for efficiency. - """ - if self.indices is not None: - return self.indices, self.xs - else: - indices, xs = zip(*self.elements.items()) - self.indices, self.xs = np.array(indices), np.array(xs) - return self.indices, self.xs - - @property - def asymptotic_time_invariant(self): - indices, xs = self.array() - tau = np.max(np.abs(indices[:, 0]))+1 # how far out do we go? - v = np.zeros(2*tau-1) - #v[indices[:, 0]+tau-1] = xs - v[-indices[:, 0]+tau-1] = xs # switch from asymptotic ROW to asymptotic COLUMN - return asymptotic.AsymptoticTimeInvariant(v) - - @property - def T(self): - """Transpose""" - return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) - - @property - def iszero(self): - return not self.nonzero().elements - - def nonzero(self): - elements = self.elements.copy() - for im, x in self.elements.items(): - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - return SimpleSparse(elements) - - def __pos__(self): - return self - - def __neg__(self): - return SimpleSparse({im: -x for im, x in self.elements.items()}) - - def __matmul__(self, A): - if isinstance(A, SimpleSparse): - # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs - return multiply_rs_rs(self, A) - elif isinstance(A, np.ndarray): - # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing - indices, xs = self.array() - if A.ndim == 2: - return multiply_rs_matrix(indices, xs, A) - elif A.ndim == 1: - return multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] - else: - return NotImplemented - else: - return NotImplemented - - def __rmatmul__(self, A): - # multiplication rule when this object is on right (will only be called when left is matrix) - # for simplicity, just use transpose to reduce this to previous cases - return (self.T @ A.T).T - - def __add__(self, A): - if isinstance(A, SimpleSparse): - # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap - elements = self.elements.copy() - for im, x in A.elements.items(): - if im in elements: - elements[im] += x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = x - return SimpleSparse(elements) - else: - # add SimpleSparse to T*T matrix - if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: - return NotImplemented - T = A.shape[0] - - # fancy trick to do this efficiently by writing A as flat vector - # then (i, m) can be mapped directly to NumPy slicing! - A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy - for (i, m), x in self.elements.items(): - if i < 0: - A[T * (-i) + (T + 1) * m::T + 1] += x - else: - A[i + (T + 1) * m:(T - i) * T:T + 1] += x - return A.reshape((T, T)) - - def __radd__(self, A): - try: - return self + A - except: - print(self) - print(A) - raise - - def __sub__(self, A): - # slightly inefficient implementation with temporary for simplicity - return self + (-A) - - def __rsub__(self, A): - return -self + A - - def __mul__(self, a): - if not np.isscalar(a): - return NotImplemented - return SimpleSparse({im: a * x for im, x in self.elements.items()}) - - def __rmul__(self, a): - return self * a - - def __repr__(self): - formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' - return f'SimpleSparse({formatted})' - - def __eq__(self, s): - return self.elements == s.elements - - -def multiply_basis(t1, t2): - """Matrix multiplication operation mapping two sparse basis elements to another.""" - # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with - # signs of i and j flipped to reflect different sign convention used here - i, m = t1 - j, n = t2 - k = i + j - if i >= 0: - if j >= 0: - l = max(m, n - i) - elif k >= 0: - l = max(m, n - k) - else: - l = max(m + k, n) - else: - if j <= 0: - l = max(m + j, n) - else: - l = max(m, n) + min(-i, j) - return k, l - - -def multiply_rs_rs(s1, s2): - """Matrix multiplication operation on two SimpleSparse objects.""" - # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, - # add all pairwise products to get overall product - elements = {} - for im, x in s1.elements.items(): - for jn, y in s2.elements.items(): - kl = multiply_basis(im, jn) - if kl in elements: - elements[kl] += x * y - else: - elements[kl] = x * y - return SimpleSparse(elements) - - -@njit -def multiply_rs_matrix(indices, xs, A): - """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. - Much more computationally demanding than multiplying two SimpleSparse (which is almost - free with simple analytical formula), so we implement as jitted function.""" - n = indices.shape[0] - T = A.shape[0] - S = A.shape[1] - Aout = np.zeros((T, S)) - - for count in range(n): - # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' - # was stored in 'indices' and 'xs' - i = indices[count, 0] - m = indices[count, 1] - x = xs[count] - - # loop faster than vectorized when jitted - # directly use def of basis element (i, m), displacement of i and ignore first m - if i == 0: - for t in range(m, T): - for s in range(S): - Aout[t, s] += x * A[t, s] - elif i > 0: - for t in range(m, T - i): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - else: - for t in range(m - i, T): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - return Aout - - -class IdentityMatrix: - """Simple identity matrix class with which we can initialize chain_jacobians and forward_accumulate, - avoiding costly explicit construction of and operations on identity matrices.""" - __array_priority__ = 10_000 - - def sparse(self): - """Equivalent SimpleSparse representation, less efficient operations but more general.""" - return sim.SimpleSparse({(0, 0): 1}) - - def matrix(self, T): - return np.eye(T) - - def __matmul__(self, other): - """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" - return copy.deepcopy(other) - - def __rmatmul__(self, other): - return copy.deepcopy(other) - - def __mul__(self, a): - return a*self.sparse() - - def __rmul__(self, a): - return self.sparse()*a - - def __add__(self, x): - return self.sparse() + x - - def __radd__(self, x): - return x + self.sparse() - - def __sub__(self, x): - return self.sparse() - x - - def __rsub__(self, x): - return x - self.sparse() - - def __neg__(self): - return -self.sparse() - - def __pos__(self): - return self - - def __repr__(self): - return 'IdentityMatrix' \ No newline at end of file diff --git a/sequence_jacobian/utilities/__init__.py b/sequence_jacobian/utilities/__init__.py index e8d1bab..ba3a615 100644 --- a/sequence_jacobian/utilities/__init__.py +++ b/sequence_jacobian/utilities/__init__.py @@ -1,3 +1,3 @@ """Utilities relating to: interpolation, forward step/transition, grids and Markov chains, solvers, sorting, etc.""" -from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers +from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers, special_matrices diff --git a/sequence_jacobian/utilities/special_matrices.py b/sequence_jacobian/utilities/special_matrices.py new file mode 100644 index 0000000..268b7c1 --- /dev/null +++ b/sequence_jacobian/utilities/special_matrices.py @@ -0,0 +1,339 @@ +"""Matrices with special structure, which work with simple, efficient rules""" + +from .. import asymptotic +import numpy as np +from numba import njit +import copy + + +class IdentityMatrix: + """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, + use to initialize Jacobian of a variable wrt itself""" + __array_priority__ = 10_000 + + def sparse(self): + """Equivalent SimpleSparse representation, less efficient operations but more general.""" + return SimpleSparse({(0, 0): 1}) + + def matrix(self, T): + return np.eye(T) + + def __matmul__(self, other): + """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" + return copy.deepcopy(other) + + def __rmatmul__(self, other): + return copy.deepcopy(other) + + def __mul__(self, a): + return a*self.sparse() + + def __rmul__(self, a): + return self.sparse()*a + + def __add__(self, x): + return self.sparse() + x + + def __radd__(self, x): + return x + self.sparse() + + def __sub__(self, x): + return self.sparse() - x + + def __rsub__(self, x): + return x - self.sparse() + + def __neg__(self): + return -self.sparse() + + def __pos__(self): + return self + + def __repr__(self): + return 'IdentityMatrix' + + +class ZeroMatrix: + """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, + use to indicate no""" + __array_priority__ = 10_000 + + def matrix(self, T): + return np.zeros((T,T)) + + def __matmul__(self, other): + if isinstance(other, np.ndarray) and other.ndim == 1: + return np.zeros_like(other) + else: + return self + + def __rmatmul__(self, other): + return self @ other + + def __mul__(self, a): + return self + + def __rmul__(self, a): + return self + + def __add__(self, x): + return copy.deepcopy(x) + + def __radd__(self, x): + return copy.deepcopy(x) + + def __sub__(self, x): + return -copy.deepcopy(x) + + def __rsub__(self, x): + return copy.deepcopy(x) + + def __neg__(self): + return self + + def __pos__(self): + return self + + def __repr__(self): + return 'ZeroMatrix' + + +class SimpleSparse: + """Efficient representation of sparse linear operators, which are linear combinations of basis + operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s + (measured by # above main diagonal) and m is number of initial entries missing. + + Examples of such basis operators: + - (0, 0) is identity operator + - (0, 2) is identity operator with first two '1's on main diagonal missing + - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator + - (-1, 1) has 1s on diagonal below main diagonal, except first column + + The linear combination of these basis operators that makes up a given SimpleSparse object is + stored as a dict 'elements' mapping (i, m) -> x. + + The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need + the more general basis (i, m) to ensure closure under multiplication. + + These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space + Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation + for writing SimpleBlock functions. + + The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix + operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less + interchangeably with ordinary NumPy matrices. + """ + + # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules + __array_priority__ = 1000 + + def __init__(self, elements): + self.elements = elements + self.indices, self.xs = None, None + + @staticmethod + def from_simple_diagonals(elements): + """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" + return SimpleSparse({(i, 0): x for i, x in elements.items()}) + + def matrix(self, T): + """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" + return self + np.zeros((T, T)) + + def array(self): + """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) + and one size-N array of floats with entries x. + + This is needed for Numba to take as input. Cache for efficiency. + """ + if self.indices is not None: + return self.indices, self.xs + else: + indices, xs = zip(*self.elements.items()) + self.indices, self.xs = np.array(indices), np.array(xs) + return self.indices, self.xs + + @property + def asymptotic_time_invariant(self): + indices, xs = self.array() + tau = np.max(np.abs(indices[:, 0]))+1 # how far out do we go? + v = np.zeros(2*tau-1) + #v[indices[:, 0]+tau-1] = xs + v[-indices[:, 0]+tau-1] = xs # switch from asymptotic ROW to asymptotic COLUMN + return asymptotic.AsymptoticTimeInvariant(v) + + @property + def T(self): + """Transpose""" + return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) + + @property + def iszero(self): + return not self.nonzero().elements + + def nonzero(self): + elements = self.elements.copy() + for im, x in self.elements.items(): + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + return SimpleSparse(elements) + + def __pos__(self): + return self + + def __neg__(self): + return SimpleSparse({im: -x for im, x in self.elements.items()}) + + def __matmul__(self, A): + if isinstance(A, SimpleSparse): + # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs + return multiply_rs_rs(self, A) + elif isinstance(A, np.ndarray): + # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing + indices, xs = self.array() + if A.ndim == 2: + return multiply_rs_matrix(indices, xs, A) + elif A.ndim == 1: + return multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] + else: + return NotImplemented + else: + return NotImplemented + + def __rmatmul__(self, A): + # multiplication rule when this object is on right (will only be called when left is matrix) + # for simplicity, just use transpose to reduce this to previous cases + return (self.T @ A.T).T + + def __add__(self, A): + if isinstance(A, SimpleSparse): + # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap + elements = self.elements.copy() + for im, x in A.elements.items(): + if im in elements: + elements[im] += x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = x + return SimpleSparse(elements) + else: + # add SimpleSparse to T*T matrix + if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: + return NotImplemented + T = A.shape[0] + + # fancy trick to do this efficiently by writing A as flat vector + # then (i, m) can be mapped directly to NumPy slicing! + A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy + for (i, m), x in self.elements.items(): + if i < 0: + A[T * (-i) + (T + 1) * m::T + 1] += x + else: + A[i + (T + 1) * m:(T - i) * T:T + 1] += x + return A.reshape((T, T)) + + def __radd__(self, A): + try: + return self + A + except: + print(self) + print(A) + raise + + def __sub__(self, A): + # slightly inefficient implementation with temporary for simplicity + return self + (-A) + + def __rsub__(self, A): + return -self + A + + def __mul__(self, a): + if not np.isscalar(a): + return NotImplemented + return SimpleSparse({im: a * x for im, x in self.elements.items()}) + + def __rmul__(self, a): + return self * a + + def __repr__(self): + formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' + return f'SimpleSparse({formatted})' + + def __eq__(self, s): + return self.elements == s.elements + + +def multiply_basis(t1, t2): + """Matrix multiplication operation mapping two sparse basis elements to another.""" + # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with + # signs of i and j flipped to reflect different sign convention used here + i, m = t1 + j, n = t2 + k = i + j + if i >= 0: + if j >= 0: + l = max(m, n - i) + elif k >= 0: + l = max(m, n - k) + else: + l = max(m + k, n) + else: + if j <= 0: + l = max(m + j, n) + else: + l = max(m, n) + min(-i, j) + return k, l + + +def multiply_rs_rs(s1, s2): + """Matrix multiplication operation on two SimpleSparse objects.""" + # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, + # add all pairwise products to get overall product + elements = {} + for im, x in s1.elements.items(): + for jn, y in s2.elements.items(): + kl = multiply_basis(im, jn) + if kl in elements: + elements[kl] += x * y + else: + elements[kl] = x * y + return SimpleSparse(elements) + + +@njit +def multiply_rs_matrix(indices, xs, A): + """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. + Much more computationally demanding than multiplying two SimpleSparse (which is almost + free with simple analytical formula), so we implement as jitted function.""" + n = indices.shape[0] + T = A.shape[0] + S = A.shape[1] + Aout = np.zeros((T, S)) + + for count in range(n): + # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' + # was stored in 'indices' and 'xs' + i = indices[count, 0] + m = indices[count, 1] + x = xs[count] + + # loop faster than vectorized when jitted + # directly use def of basis element (i, m), displacement of i and ignore first m + if i == 0: + for t in range(m, T): + for s in range(S): + Aout[t, s] += x * A[t, s] + elif i > 0: + for t in range(m, T - i): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + else: + for t in range(m - i, T): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + return Aout + + From e00fea8f0f1f03687e051fe0f376e6b455269115 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Sat, 30 Jan 2021 16:57:58 -0600 Subject: [PATCH 002/288] new JacobianDict object that conveniently wraps nested dicts of Jacobians, returned by get_G, jacobian.py rewritten to accommodate it --- sequence_jacobian/jacobian.py | 266 ++++++++++++------ .../utilities/special_matrices.py | 18 +- tests/base/test_jacobian.py | 8 +- 3 files changed, 198 insertions(+), 94 deletions(-) diff --git a/sequence_jacobian/jacobian.py b/sequence_jacobian/jacobian.py index 255dec5..e49f3a5 100644 --- a/sequence_jacobian/jacobian.py +++ b/sequence_jacobian/jacobian.py @@ -6,8 +6,7 @@ from . import utilities as utils from . import asymptotic -from .blocks import simple_block as sim - +from .utilities.special_matrices import IdentityMatrix, ZeroMatrix '''Part 1: High-level convenience routines: - get_H_U : get H_U matrix mapping all unknowns to all targets @@ -53,7 +52,8 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=N if not asymptotic: # pack these n_u^2 matrices, each T*T, into a single matrix - return pack_jacobians(H_U_unpacked, unknowns, targets, T) + return H_U_unpacked[targets, unknowns].pack(T) + #return pack_jacobians(H_U_unpacked, unknowns, targets, T) else: # pack these n_u^2 AsymptoticTimeInvariant objects into a single (2*Tpost-1,n_u,n_u) array if Tpost is None: @@ -109,7 +109,8 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None # step 3: solve H_UdU = -H_ZdZ for dU if H_U is None and H_U_factored is None: - H_U = pack_jacobians(H_U_unpacked, unknowns, targets, T) + H_U = H_U_unpacked[targets, unknowns].pack(T) + #H_U = pack_jacobians(H_U_unpacked, unknowns, targets, T) H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) @@ -169,19 +170,21 @@ def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, # step 3: solve for G^U, unpack if H_U is None and H_U_factored is None: - H_U = pack_jacobians(J_curlyH_U, unknowns, targets, T) - H_Z = pack_jacobians(J_curlyH_Z, exogenous, targets, T) + H_U = J_curlyH_U[targets, unknowns].pack(T) + #H_U = pack_jacobians(J_curlyH_U, unknowns, targets, T) + H_Z = J_curlyH_Z[targets, exogenous].pack(T) + #H_Z = pack_jacobians(J_curlyH_Z, exogenous, targets, T) if H_U_factored is None: - G_U = unpack_jacobians(-np.linalg.solve(H_U, H_Z), exogenous, unknowns, T) + G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) else: - G_U = unpack_jacobians(-utils.misc.factored_solve(H_U_factored, H_Z), exogenous, unknowns, T) + G_U = JacobianDict.unpack(-utils.misc.factored_solve(H_U_factored, H_Z), unknowns, exogenous, T) # step 4: forward accumulation to get all outputs starting with G_U # by default, don't calculate targets! curlyJs = [G_U] + curlyJs if outputs is None: - outputs = set().union(*(curlyJ.keys() for curlyJ in curlyJs)) - set(targets) + outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) @@ -206,7 +209,7 @@ def get_G_asymptotic(block_list, exogenous, unknowns, targets, T, ss=None, outpu # by default, don't calculate targets! curlyJs = [G_U] + curlyJs if outputs is None: - outputs = set().union(*(curlyJ.keys() for curlyJ in curlyJs)) - set(targets) + outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) @@ -265,7 +268,7 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=N if not jac: continue else: - curlyJs.append(jac) + curlyJs.append(JacobianDict(jac)) return curlyJs, required @@ -311,99 +314,44 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): if jacflag: # Jacobians of inputs with respect to themselves are the identity, initialize with this - out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} + #out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} + out = JacobianDict.identity(inputs) else: out = inputs.copy() # iterate through curlyJs, in what is presumed to be a topologically sorted order for curlyJ in curlyJs: + curlyJ = JacobianDict(curlyJ).complete() if alloutputs is not None: # if we want specific list of outputs, restrict curlyJ to that before continuing - curlyJ = {k: v for k, v in curlyJ.items() if k in alloutputs} + #curlyJ = {k: v for k, v in curlyJ.items() if k in alloutputs} + curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] if jacflag: - out.update(compose_jacobians(out, curlyJ)) + #out.update(compose_jacobians(out, curlyJ)) + out.update(curlyJ.compose(out)) else: - out.update(apply_jacobians(curlyJ, out)) + #out.update(apply_jacobians(curlyJ, out)) + out.update(curlyJ.apply(out)) if outputs is not None: # if we want specific list of outputs, restrict to that # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) - return {k: out[k] for k in outputs if k in out} + #return {k: out[k] for k in outputs if k in out} + return out[[k for k in outputs if k in out.outputs]] else: - if jacflag: - # default behavior for Jacobian case: return all Jacobians we used/calculated along the way - # except the (redundant) IdentityMatrix objects mapping inputs to themselves - return {k: v for k, v in out.items() if k not in inputs} - else: - # default behavior for case where we're calculating paths: return everything, including inputs - return out + return out + # if jacflag: + # # default behavior for Jacobian case: return all Jacobians we used/calculated along the way + # # except the (redundant) IdentityMatrix objects mapping inputs to themselves + # return {k: v for k, v in out.items() if k not in inputs} + # else: + # # default behavior for case where we're calculating paths: return everything, including inputs + # return out '''Part 2: Somewhat lower-level routines for handling Jacobians''' -def chain_jacobians(jacdicts, inputs): - """Obtain complete Jacobian of every output in jacdicts with respect to inputs, by applying chain rule.""" - cumulative_jacdict = {i: {i: IdentityMatrix()} for i in inputs} - for jacdict in jacdicts: - cumulative_jacdict.update(compose_jacobians(cumulative_jacdict, jacdict)) - return cumulative_jacdict - - -def compose_jacobians(jacdict2, jacdict1): - """Compose Jacobians via the chain rule.""" - jacdict = {} - for output, innerjac1 in jacdict1.items(): - jacdict[output] = {} - for middle, jac1 in innerjac1.items(): - innerjac2 = jacdict2.get(middle, {}) - for inp, jac2 in innerjac2.items(): - if inp in jacdict[output]: - jacdict[output][inp] += jac1 @ jac2 - else: - jacdict[output][inp] = jac1 @ jac2 - return jacdict - - -def apply_jacobians(jacdict, indict): - """Apply Jacobians in jacdict to indict to obtain outputs.""" - outdict = {} - for myout, innerjacdict in jacdict.items(): - for myin, jac in innerjacdict.items(): - if myin in indict: - if myout in outdict: - outdict[myout] += jac @ indict[myin] - else: - outdict[myout] = jac @ indict[myin] - - return outdict - - -def pack_jacobians(jacdict, inputs, outputs, T): - """If we have T*T jacobians from nI inputs to nO outputs in jacdict, combine into (nO*T)*(nI*T) jacobian matrix.""" - nI, nO = len(inputs), len(outputs) - - outjac = np.empty((nO * T, nI * T)) - for iO in range(nO): - subdict = jacdict.get(outputs[iO], {}) - for iI in range(nI): - outjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(subdict.get(inputs[iI], - np.zeros((T, T))), T) - return outjac - - -def unpack_jacobians(bigjac, inputs, outputs, T): - """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" - nI, nO = len(inputs), len(outputs) - - jacdict = {} - for iO in range(nO): - jacdict[outputs[iO]] = {} - for iI in range(nI): - jacdict[outputs[iO]][inputs[iI]] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] - return jacdict - - def pack_asymptotic_jacobians(jacdict, inputs, outputs, tau): """If we have -(tau-1),...,(tau-1) AsymptoticTimeInvariant Jacobians (or SimpleSparse) from nI inputs to nO outputs in jacdict, combine into (2*tau-1,nO,nI) array A""" @@ -465,3 +413,151 @@ def make_ATI_v(x, tau): return x.v +"""Experimental new jacdict feature""" + +class NestedDict: + def __init__(self, nesteddict, outputs=None, inputs=None): + if isinstance(nesteddict, NestedDict): + self.nesteddict = nesteddict.nesteddict + self.outputs = nesteddict.outputs + self.inputs = nesteddict.inputs + else: + self.nesteddict = nesteddict + if outputs is None: + outputs = list(nesteddict.keys()) + if inputs is None: + inputs = [] + for v in nesteddict.values(): + inputs.extend(list(v)) + inputs = deduplicate(inputs) + + self.outputs = list(outputs) + self.inputs = list(inputs) + + def __repr__(self): + return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' + + def __iter__(self): + return iter(self.outputs) + + def __getitem__(self, x): + if isinstance(x, str): + # case 1: just a single output, give subdict + return self.nesteddict[x] + elif isinstance(x, tuple): + # case 2: tuple, referring to output and input + o, i = x + o = self.outputs if o == slice(None, None, None) else o + i = self.inputs if i == slice(None, None, None) else i + if isinstance(o, str): + if isinstance(i, str): + # case 2a: one output, one input, return single Jacobian + return self.nesteddict[o][i] + else: + # case 2b: one output, multiple inputs, return dict + return {ii: self.nesteddict[o][ii] for ii in i} + else: + # case 2c: multiple outputs, one or more inputs, return JacobianDict with outputs o and inputs i + i = (i,) if isinstance(i, str) else i + return JacobianDict({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) + elif isinstance(x, list) or isinstance(x, set): + # case 3: assume that list or set refers just to outputs, get all of those + return JacobianDict({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) + else: + raise ValueError(f'Tried to get impermissible item {x}') + + def get(self, *args, **kwargs): + # this is for compatibilty, not a huge fan + return self.nesteddict.get(*args, **kwargs) + + + def update(self, J): + if set(self.inputs) != set(J.inputs): + raise ValueError(f'Cannot merge JacobianDicts with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') + if not set(self.outputs).isdisjoint(J.outputs): + raise ValueError(f'Cannot merge JacobianDicts with overlapping outputs {set(self.outputs) & set(J.outputs)}') + self.outputs = self.outputs + J.outputs + self.nesteddict = {**self.nesteddict, **J.nesteddict} + + def complete(self, filler): + nesteddict = {} + for o in self.outputs: + nesteddict[o] = dict(self.nesteddict[o]) + for i in self.inputs: + if i not in nesteddict[o]: + nesteddict[o][i] = filler + return JacobianDict(nesteddict, self.outputs, self.inputs) + + +def deduplicate(mylist): + """Remove duplicates while otherwise maintaining order""" + return list(dict.fromkeys(mylist)) + + +class JacobianDict(NestedDict): + @staticmethod + def identity(ks): + return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() + + def complete(self): + return super().complete(ZeroMatrix()) + + def __matmul__(self, x): + if isinstance(x, JacobianDict): + return self.compose(x) + else: + return self.apply(x) + + def compose(self, J): + o_list = self.outputs + m_list = tuple(set(self.inputs) & set(J.outputs)) + i_list = J.inputs + + J_om = self.nesteddict + J_mi = J.nesteddict + J_oi = {} + + for o in o_list: + J_oi[o] = {} + for i in i_list: + Jout = ZeroMatrix() + for m in m_list: + J_om[o][m] + J_mi[m][i] + Jout += J_om[o][m] @ J_mi[m][i] + J_oi[o][i] = Jout + + return JacobianDict(J_oi, o_list, i_list) + + def apply(self, x): + # assume that all entries in x have some length T, and infer it + T = len(next(iter(x.values()))) + + inputs = x.keys() & set(self.inputs) + J_oi = self.nesteddict + y = {} + + for o in self.outputs: + y[o] = np.zeros(T) + for i in inputs: + y[o] += J_oi[o][i] @ x[i] + + return y + + def pack(self, T): + J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) + for iO, O in enumerate(self.outputs): + for iI, I in enumerate(self.inputs): + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(self[O, I], T) + return J + + @staticmethod + def unpack(bigjac, outputs, inputs, T): + """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" + jacdict = {} + for iO, O in enumerate(outputs): + jacdict[O] = {} + for iI, I in enumerate(inputs): + jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] + return JacobianDict(jacdict, outputs, inputs) + diff --git a/sequence_jacobian/utilities/special_matrices.py b/sequence_jacobian/utilities/special_matrices.py index 268b7c1..c9f6716 100644 --- a/sequence_jacobian/utilities/special_matrices.py +++ b/sequence_jacobian/utilities/special_matrices.py @@ -55,9 +55,12 @@ def __repr__(self): class ZeroMatrix: """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, - use to indicate no""" + use in common case where some outputs don't depend on inputs""" __array_priority__ = 10_000 + def sparse(self): + return SimpleSparse({(0, 0): 0}) + def matrix(self, T): return np.zeros((T,T)) @@ -76,17 +79,18 @@ def __mul__(self, a): def __rmul__(self, a): return self + # copies seem inefficient here, try to live without them def __add__(self, x): - return copy.deepcopy(x) + return x def __radd__(self, x): - return copy.deepcopy(x) + return x def __sub__(self, x): - return -copy.deepcopy(x) + return -x def __rsub__(self, x): - return copy.deepcopy(x) + return x def __neg__(self): return self @@ -97,6 +101,10 @@ def __pos__(self): def __repr__(self): return 'ZeroMatrix' + @property + def asymptotic_time_invariant(self): + return self.sparse().asymptotic_time_invariant + class SimpleSparse: """Efficient representation of sparse linear operators, which are linear combinations of basis diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 0e2e969..f477aa5 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -44,11 +44,11 @@ def test_hank_jac(one_asset_hank_model): curlyJs, required = jacobian.curlyJ_sorted(blocks, unknowns+exogenous, ss, T) J_curlyH_U = jacobian.forward_accumulate(curlyJs, unknowns, targets, required) J_curlyH_Z = jacobian.forward_accumulate(curlyJs, exogenous, targets, required) - H_U = jacobian.pack_jacobians(J_curlyH_U, unknowns, targets, T) - H_Z = jacobian.pack_jacobians(J_curlyH_Z, exogenous, targets, T) - G_U = jacobian.unpack_jacobians(-np.linalg.solve(H_U, H_Z), exogenous, unknowns, T) + H_U = J_curlyH_U[targets, unknowns].pack(T) + H_Z = J_curlyH_Z[targets, exogenous].pack(T) + G_U = jacobian.JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) curlyJs = [G_U] + curlyJs - outputs = set().union(*(curlyJ.keys() for curlyJ in curlyJs)) - set(targets) + outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) G = jacobian.forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) for o in G: From b905c89940635eefa117a1c29a376685026e7748 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Sat, 30 Jan 2021 18:08:37 -0600 Subject: [PATCH 003/288] minor fixes to NestedDict and JacobianDict, plus .addinput() --- sequence_jacobian/jacobian.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/jacobian.py b/sequence_jacobian/jacobian.py index e49f3a5..cde8487 100644 --- a/sequence_jacobian/jacobian.py +++ b/sequence_jacobian/jacobian.py @@ -440,6 +440,12 @@ def __repr__(self): def __iter__(self): return iter(self.outputs) + def __or__(self, other): + # non-in-place merge: make a copy, then update + merged = type(self)(self.nesteddict, self.outputs, self.inputs) + merged.update(other) + return merged + def __getitem__(self, x): if isinstance(x, str): # case 1: just a single output, give subdict @@ -457,12 +463,12 @@ def __getitem__(self, x): # case 2b: one output, multiple inputs, return dict return {ii: self.nesteddict[o][ii] for ii in i} else: - # case 2c: multiple outputs, one or more inputs, return JacobianDict with outputs o and inputs i + # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i i = (i,) if isinstance(i, str) else i - return JacobianDict({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) + return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) elif isinstance(x, list) or isinstance(x, set): # case 3: assume that list or set refers just to outputs, get all of those - return JacobianDict({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) + return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) else: raise ValueError(f'Tried to get impermissible item {x}') @@ -470,12 +476,11 @@ def get(self, *args, **kwargs): # this is for compatibilty, not a huge fan return self.nesteddict.get(*args, **kwargs) - def update(self, J): if set(self.inputs) != set(J.inputs): - raise ValueError(f'Cannot merge JacobianDicts with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') + raise ValueError(f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') if not set(self.outputs).isdisjoint(J.outputs): - raise ValueError(f'Cannot merge JacobianDicts with overlapping outputs {set(self.outputs) & set(J.outputs)}') + raise ValueError(f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') self.outputs = self.outputs + J.outputs self.nesteddict = {**self.nesteddict, **J.nesteddict} @@ -486,7 +491,7 @@ def complete(self, filler): for i in self.inputs: if i not in nesteddict[o]: nesteddict[o][i] = filler - return JacobianDict(nesteddict, self.outputs, self.inputs) + return type(self)(nesteddict, self.outputs, self.inputs) def deduplicate(mylist): @@ -502,6 +507,11 @@ def identity(ks): def complete(self): return super().complete(ZeroMatrix()) + def addinputs(self): + """Add any inputs that were not already in output list as outputs, with the identity""" + inputs = [x for x in self.inputs if x not in self.outputs] + return self | JacobianDict.identity(inputs) + def __matmul__(self, x): if isinstance(x, JacobianDict): return self.compose(x) From 46a3b8e62c01ccb4170da5d3eb570d2a4a700ec6 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 14:11:16 -0600 Subject: [PATCH 004/288] Complete intro_to_sequence_jacobian.ipynb notebook --- notebooks/intro_to_sequence_jacobian.ipynb | 736 ++++++++++++++++++++- 1 file changed, 725 insertions(+), 11 deletions(-) diff --git a/notebooks/intro_to_sequence_jacobian.ipynb b/notebooks/intro_to_sequence_jacobian.ipynb index 9c82d5d..510a744 100644 --- a/notebooks/intro_to_sequence_jacobian.ipynb +++ b/notebooks/intro_to_sequence_jacobian.ipynb @@ -16,14 +16,14 @@ " 1. `SimpleBlock`\n", " 2. `HetBlock`\n", " 3. `SolvedBlock`\n", - "2. The main functions of `sequence-jacobian`\n", + "2. The primary functions of `sequence-jacobian`\n", " - `steady_state`\n", " - `get_G`\n", - " - `td_map`\n", + " - `td_solve`\n", "\n", "The notebook accompanies the working paper by Auclert, Bardóczy, Rognlie, Straub (2019): \"Using the Sequence-Space Jacobian to Solve and Estimate Heterogeneous-Agent Models\". Please see the [Github repository](https://github.com/shade-econ/sequence-jacobian) for more information and code.\n", "\n", - "Also, be sure to check out the other model notebooks in this directory to see some applications of `sequence-jacobian`." + "Also, this notebook borrows material from the other model example notebooks so be sure to check out them out to see a more complete model solution workflow using `sequence-jacobian`." ] }, { @@ -119,14 +119,14 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", + "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj\n", - "from sequence_jacobian import simple, het, solved\n", + "from sequence_jacobian import simple, het, solved, steady_state, get_G, td_solve\n", "from sequence_jacobian import utilities as utils" ] }, @@ -137,6 +137,17 @@ "# 1 How do `Block` objects work?" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first step of solving a model is to come up with a \"Directed Acyclic Graph\" (**DAG**) representation for it and specify its building `Block`s.\n", + "\n", + "`Block` objects are collections of the model's equilibrium conditions, typically grouped as the conceptual pieces of the model, e.g. firm problem, household problem, market clearing conditions. Each `Block` takes a set of parameters and variables as inputs and produces another set as outputs.\n", + "\n", + "A model's equilibrium conditions and associated unknown variables can be re-written as a directed acyclic graph (DAG) of `Block`s, whose structure can be exploited for fast computing the model's steady state and solving for linear and non-linear dynamic responses of endogenous variables to shocks." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -318,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -426,7 +437,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -519,12 +530,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Sometimes within the structure of the DAG we can specify a set of equations that constitute a smaller, self-contained DAG. One example of this is the New Keynesian Phillips Curve, which is a `SolvedBlock` in our two-asset heterogeneous agent model." + "Sometimes within the structure of the directed acyclic graph (DAG) we can specify a set of equations that constitute a smaller, self-contained DAG. One example of this is the New Keynesian Phillips Curve, which is a `SolvedBlock` in our two-asset heterogeneous agent model." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -565,7 +576,710 @@ { "cell_type": "markdown", "metadata": {}, - "source": [] + "source": [ + "### Steady state with a standard DAG" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For a given DAG, `steady_state` solves for the steady state of a model. Although it is not required for users to use `steady_state` in order to utilize the other functions provided by the `sequence-jacobian` toolkit, it may be convenient to do so since the primary functions in `sequence-jacobian` require the basically the same set of arguments.\n", + "\n", + "Consider the following set of `Block` objects that contain the equilibrium conditions for the standard real business cycle (RBC) model." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "@simple\n", + "def rbc_firm(K, L, Z, alpha, delta):\n", + " r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta\n", + " w = (1 - alpha) * Z * (K(-1) / L) ** alpha\n", + " Y = Z * K(-1) ** alpha * L ** (1 - alpha)\n", + " return r, w, Y\n", + "\n", + "@simple\n", + "def rbc_household(K, L, w, eis, frisch, vphi, delta):\n", + " C = (w / vphi / L ** (1 / frisch)) ** eis\n", + " I = K - (1 - delta) * K(-1)\n", + " return C, I\n", + "\n", + "@simple\n", + "def rbc_mkt_clearing(r, C, Y, I, K, L, w, eis, beta):\n", + " goods_mkt = Y - C - I\n", + " euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis)\n", + " walras = C + K - (1 + r) * K(-1) - w * L\n", + " return goods_mkt, euler, walras" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we would like to calibrate the steady state such that output $Y$ is normalized to 1, and the euler equation and goods market clearing hold. Given we have three calibration `target` variables, we need three free or `unknown` variables to hit those targets. Beyond that, we might like to specify some fixed variables or parameters in steady state, which we can specify in the `calibration` dictionary.\n", + "\n", + "Once we have provided all of these arguments to `steady_state` and specified which root-finding algorithm we would like it to use in the keyword argument `solver`, it will solve for the steady state of the DAG." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "# Solving for the steady state as a standard DAG\n", + "rbc_calibration = {\"L\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11, \"beta\": 1/(1 + 0.01)}\n", + "rbc_blocks = [rbc_household, rbc_firm, rbc_mkt_clearing]\n", + "rbc_ss_unknowns = {\"vphi\": 0.9, \"K\": 2., \"Z\": 1.}\n", + "rbc_ss_targets = {\"euler\": 0., \"goods_mkt\": 0., \"Y\": 1.}\n", + "rbc_ss = steady_state(rbc_blocks, rbc_calibration, rbc_ss_unknowns, rbc_ss_targets, solver=\"broyden_custom\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Steady state with an analytical solution-augmented DAG\n", + "\n", + "In this alternative way of solving for the steady state we will make use of a new kind of block called a `HelperBlock`, whose purpose is to provide a more flexible way of using the sequence-jacobian toolkit to calibrate a model's steady state. \n", + "\n", + "A `HelperBlock` works identically to the `SimpleBlock`s that we constructed above using the decorator `@simple` for the end-user, but using the decorator `@helper` instead. Under the hood the sequence-jacobian toolkit handles them differently when the blocks are sorted/used outside of the steady state.\n", + "\n", + "In the case of the RBC model, given our choice of fixed $r = 0.01$, normalizing to $Y = 1$ lets us provide a *complete* analytical characterization of the steady state. In steps: we choose the discount rate $\\beta$ to hit a given real interest rate $r$, the disutility of labor $\\varphi$ to hit labor $L=1$, and normalize TFP $Z$ to get output $Y=1$." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from sequence_jacobian import helper\n", + "\n", + "@helper\n", + "def rbc_steady_state_solution(r, eis, delta, alpha):\n", + " rk = r + delta\n", + " Z = (rk / alpha) ** alpha # normalize so that Y=1\n", + " K = (alpha * Z / rk) ** (1 / (1 - alpha))\n", + " Y = Z * K ** alpha\n", + " w = (1 - alpha) * Z * K ** alpha\n", + " I = delta * K\n", + " C = Y - I\n", + " beta = 1 / (1 + r)\n", + " vphi = w * C ** (-1 / eis)\n", + "\n", + " return Z, K, Y, w, I, C, beta, vphi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because the solution is entirely analytical it is not necessary to include any `unknown`s. Also, we can specify `solver=\"solved\"` to avoid using a root-finding algorithm. However, given the present structure of the code, we still require that you provide some target values to verify that the steady state given by the analytical solution indeed produces one that satisfies some set of target equations, like the euler equation, goods market clearing, or Walras' Law, as a gut-check to ensure we don't proceed forward with something we don't want!\n", + "\n", + "Also, if all of your targets are implicit functions, i.e. you want to target their values equal to 0, you can use a list of their names instead of a dict(ionary)." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "rbc_calibration_helper = {\"L\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11}\n", + "rbc_blocks_helper = [rbc_household, rbc_firm, rbc_mkt_clearing, rbc_steady_state_solution]\n", + "rbc_ss_unknowns_helper = {}\n", + "rbc_ss_targets_helper = [\"euler\", \"goods_mkt\"]\n", + "rbc_ss_helper = steady_state(rbc_blocks_helper, rbc_calibration_helper, rbc_ss_unknowns_helper, rbc_ss_targets_helper, solver=\"solved\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'L': 1.0,\n", + " 'r': 0.009999999999999995,\n", + " 'eis': 1.0,\n", + " 'frisch': 1.0,\n", + " 'delta': 0.025,\n", + " 'alpha': 0.11,\n", + " 'Z': 0.8816460975214567,\n", + " 'K': 3.1428571428571432,\n", + " 'Y': 1.0,\n", + " 'w': 0.8900000000000001,\n", + " 'I': 0.07857142857142874,\n", + " 'C': 0.9214285714285713,\n", + " 'beta': 0.9900990099009901,\n", + " 'vphi': 0.9658914728682173,\n", + " 'euler': 0.0,\n", + " 'goods_mkt': 0.0,\n", + " 'walras': 0.0}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rbc_ss_helper" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Gut Check**: To verify that this steady state delivers the same thing that we got from computing it along the standard DAG, we can use one of the developer tools provided in `sequence-jacobian`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "L resid: 0.0\n", + "r resid: 9.327198735586961e-12\n", + "eis resid: 0.0\n", + "frisch resid: 0.0\n", + "delta resid: 0.0\n", + "alpha resid: 0.0\n", + "beta resid: 0.0\n", + "vphi resid: 5.6993409991434874e-11\n", + "K resid: 7.346452335355025e-10\n", + "Z resid: 5.1535109513167754e-11\n", + "w resid: 2.913902452661432e-11\n", + "Y resid: 3.274069904080079e-11\n", + "I resid: 1.836619745176904e-11\n", + "C resid: 2.4201751713803787e-11\n", + "euler resid: 1.0022205287896213e-11\n", + "goods_mkt resid: 7.530864820637362e-11\n", + "walras resid: 7.530831513946623e-11\n" + ] + } + ], + "source": [ + "import sequence_jacobian.utilities.devtools as dtools\n", + "\n", + "dtools.compare_steady_states(rbc_ss, rbc_ss_helper)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It checks out!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Steady state in the general case\n", + "\n", + "In practice, your own steady state workflow will likely be somewhere in between the two cases previously described. To compute the steady state it may be easy to analytically solve some portions of the model, but other portions may only be numerically computable, specified as a set of unknowns and targets within a DAG. This may also be desirable for performance reasons, since typically computing a portion of the DAG analytically is less costly than providing the root-finding algorithm with an additional dimension to solve.\n", + "\n", + "Thankfully, any combination of the above two methods is permissible in the `sequence-jacobian` toolkit. You need only provide the analytical solution component specific to the steady state as a `HelperBlock` in the standard list of blocks, specify your unknowns and targets, and call `steady_state`! You don't even need to swap out the `HelperBlock` from the list of blocks when computing general equilibrium Jacobians or computing non-linear transition dynamics." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.B `get_G`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once a steady state for the model is obtained, `get_G` supplies the general equilibrium Jacobian, which defines the linearized impulse responses of any set of endogenous variables `dY` to any set of exogenous shocks `dZ`.\n", + "\n", + "Several other notebooks, including `rbc.ipynb`, `krusell_smith.ipynb`, and `hank.ipynb`, go into further details of what is happening under the hood when `get_G` is called, so it should suffice to simply reiterate how `get_G` is used in each of these model contexts here." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Introducing the `JacobianDict` class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the sake of explanation let us consider the following DAG, which represents the RBC model.\n", + "\n", + "![Directed Acyclic Graph for RBC model](../figures/rbc_dag.png)\n", + "\n", + "We can call `get_G` on the list of `Block` objects and specify the `exogenous`, `unknown`, `target` variables that collectively constitute this DAG, and additionaly provide the length of time by which the variables deviate from their steady state values, `T`, and the steady state values, `ss`, to obtain an instance of the `JacobianDict` class." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rbc_G = get_G(block_list=rbc_blocks, exogenous=['Z'], unknowns=['K', 'L'], targets=['euler', 'goods_mkt'], T=300, ss=rbc_ss)\n", + "rbc_G" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As seen from its display, this `JacobianDict` can find the linearized impulse response of any of the listed `outputs` to a shock path any of its listed `inputs`. Let us create a linearized impulse response $dC = G^{C, Z} dZ$ below, where $dZ$ equals 0.01 on impact and decays at an exponential rate with a persistence of 0.8." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dOut_rbc = rbc_G[[\"C\"]] @ {\"Z\": 0.01 * 0.8 ** np.arange(300)}\n", + "\n", + "plt.plot(100 * dOut_rbc[\"C\"][:50]/rbc_ss[\"C\"], linewidth=2.5)\n", + "plt.title(r'Consumption response to TFP shock in the RBC Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because the `JacobianDict` defines the linearized relationship between any input path to any output path across time (in \"sequence space\"), it is simple to also find the response to a news shock." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dOut_rbc_news = rbc_G[[\"C\"]] @ {\"Z\": np.concatenate((np.zeros(10), 0.01 * 0.8 ** np.arange(290)))}\n", + "\n", + "plt.plot(100 * dOut_rbc_news[\"C\"][:50]/rbc_ss[\"C\"], linewidth=2.5, color=\"orange\")\n", + "plt.title(r'Consumption response to TFP news shock at t = 10 in the RBC Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Krusell Smith Example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can represent this model as a simple DAG in just 1 unknown $K$ and 1 target, asset market clearing:\n", + "\n", + "![Directed Acyclical Graph](../figures/ks_dag.png)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from sequence_jacobian.models import krusell_smith\n", + "\n", + "# Use the pre-defined blocks from the model module\n", + "# We can include the definitions of the grids for income and assets as blocks as well, hence the inclusion of\n", + "# krusell_smith.income_state_vars and krusell_smith.asset_state_vars as additional SimpleBlocks\n", + "ks_blocks = [krusell_smith.household, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars,\n", + " krusell_smith.asset_state_vars, krusell_smith.firm_steady_state_solution]\n", + "\n", + "ks_calibration = {\"eis\": 1, \"delta\": 0.025, \"alpha\": 0.11, \"rho\": 0.966, \"sigma\": 0.5, \"L\": 1.0,\n", + " \"nS\": 2, \"nA\": 10, \"amax\": 200, \"r\": 0.01}\n", + "ks_ss_unknowns = {\"beta\": (0.98/1.01, 0.999/1.01)}\n", + "ks_ss_targets = {\"K\": \"A\"}\n", + "ks_ss = steady_state(ks_blocks, ks_calibration, ks_ss_unknowns, ks_ss_targets, solver=\"brentq\")\n", + "\n", + "ks_G = get_G(block_list=ks_blocks, exogenous=['Z'], unknowns=['K'], targets=['asset_mkt'], T=300, ss=ks_ss)\n", + "\n", + "dOut_ks = ks_G[[\"C\"]] @ {\"Z\": 0.01 * 0.8 ** np.arange(300)}\n", + "\n", + "plt.plot(100 * dOut_ks[\"C\"][:50]/ks_ss[\"C\"], linewidth=2.5)\n", + "plt.title(r'Consumption response to TFP shock in the KS Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dOut_ks_news = ks_G[[\"C\"]] @ {\"Z\": np.concatenate((np.zeros(10), 0.01 * 0.8 ** np.arange(290)))}\n", + "\n", + "plt.plot(100 * dOut_ks_news[\"C\"][:50]/ks_ss[\"C\"], linewidth=2.5, color=\"orange\")\n", + "plt.title(r'Consumption response to TFP news shock at t = 10 in the KS Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### One Asset HANK Model Example" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from sequence_jacobian.models import hank\n", + "\n", + "# Use the pre-defined blocks from the model module\n", + "hank_blocks = [hank.household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc,\n", + " hank.income_state_vars, hank.asset_state_vars, hank.partial_steady_state_solution]\n", + "\n", + "hank_calibration = {\"r\": 0.005, \"rstar\": 0.005, \"eis\": 0.5, \"frisch\": 0.5, \"mu\": 1.2, \"B_Y\": 5.6,\n", + " \"rho_s\": 0.966, \"sigma_s\": 0.5, \"kappa\": 0.1, \"phi\": 1.5, \"Y\": 1, \"Z\": 1, \"L\": 1,\n", + " \"pi\": 0, \"nS\": 2, \"amax\": 150, \"nA\": 10}\n", + "hank_ss_unknowns = {\"beta\": 0.986, \"vphi\": 0.8}\n", + "hank_ss_targets = {\"asset_mkt\": 0, \"labor_mkt\": 0}\n", + "hank_ss = steady_state(hank_blocks, hank_calibration, hank_ss_unknowns, hank_ss_targets, solver=\"broyden_custom\")\n", + "\n", + "hank_G = get_G(block_list=hank_blocks, exogenous=['rstar', 'Z'], unknowns=['pi', 'w', 'Y'],\n", + " targets=['nkpc_res', 'asset_mkt', 'labor_mkt'], T=300, ss=hank_ss)\n", + "\n", + "dOut_hank = hank_G[[\"C\"]] @ {\"Z\": 0.01 * 0.8 ** np.arange(300)}\n", + "\n", + "plt.plot(100 * dOut_hank[\"C\"][:50]/hank_ss[\"C\"], linewidth=2.5)\n", + "plt.title(r'Consumption response to TFP shock in the One Asset HANK Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "dOut_hank_news = hank_G[[\"C\"]] @ {\"Z\": np.concatenate((np.zeros(10), 0.01 * 0.8 ** np.arange(290)))}\n", + "\n", + "plt.plot(100 * dOut_hank_news[\"C\"][:50]/hank_ss[\"C\"], linewidth=2.5, color=\"orange\")\n", + "plt.title(r'Consumption response to TFP news shock at t = 10 in the One Asset HANK Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.C `td_solve`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "One can also use `sequence-jacobian` to solve for non-linear impulse responses of a set of endogenous variables by providing the full path of a set of shocks. This may be useful to see if the true impulse response of a set of variables actually does scale linearly, irrespective of the size, and is symmetric across sign. We will show a few comparisons of the non-linear responses with the corresponding linear responses plotted above." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### RBC Example" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On iteration 0\n", + " max error for goods_mkt is 8.91E-04\n", + " max error for euler is 2.75E-03\n", + "On iteration 1\n", + " max error for goods_mkt is 9.21E-05\n", + " max error for euler is 4.07E-05\n", + "On iteration 2\n", + " max error for goods_mkt is 4.07E-07\n", + " max error for euler is 4.66E-07\n", + "On iteration 3\n", + " max error for goods_mkt is 5.74E-09\n", + " max error for euler is 5.76E-09\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "rbc_td = td_solve(ss=rbc_ss, block_list=rbc_blocks,\n", + " unknowns=['K', 'L'], targets=['goods_mkt', 'euler'],\n", + " Z=rbc_ss['Z'] + 0.01 * 0.8 ** np.arange(300))\n", + "dOut_rbc_nonlin = 100 * (rbc_td[\"C\"]/rbc_ss[\"C\"] - 1)\n", + "\n", + "plt.plot(100 * dOut_rbc[\"C\"][:50]/rbc_ss[\"C\"], linewidth=2.5, label=\"linear\")\n", + "plt.plot(dOut_rbc_nonlin[:50], linewidth=2.5, linestyle=\"--\", label=\"non-linear\")\n", + "plt.title(r'Consumption response to TFP shock in the RBC Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Krusell Smith Example" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On iteration 0\n", + " max error for asset_mkt is 3.43E-02\n", + "On iteration 1\n", + " max error for asset_mkt is 1.43E-05\n", + "On iteration 2\n", + " max error for asset_mkt is 3.68E-08\n", + "On iteration 3\n", + " max error for asset_mkt is 7.72E-11\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3dd3hUZfbA8e9JrySU0BI6QektYEBULCC6FuwFXcsqrr2sura1+1t7d1fFghVlRRSwgNhQBKkR6b2EGkoIIaSf3x/3gkNMwgCZ3CRzPs8zT+bWOe/kzpz73vfO+4qqYowxJniFeB2AMcYYb1kiMMaYIGeJwBhjgpwlAmOMCXKWCIwxJshZIjDGmCBniaCOEZF7ROQNr+Mw3hCR1iKiIhJWxfv9QUSu8nPdBSIysIped6CIZFbFvmoDEXlQRN73c12//ycHUucTgYhcLCKzRCRXRDaKyFciMsDruKpCeR8SVf0/Va2SgyNYHcyHsZxtv3KPtVwRKRKRQp/pV93/WanPvFwRGe/zukXuvGwR+UVE+lVt6QJPVTur6g+Hsq2bxNpXcUh79325iPzsM11PRKaKyBgRCReRFPf5VhHZKSK/i8jlFexroBvrp2Xmd3fn/xCIMgRKnU4EInIb8Dzwf0AToCXwH+BML+OqC6r6jLOuUNVTVDVOVeOAD4An906r6t/d1Tb4zItT1dN9dvGxu20S8DPwqYhINRejzhOR+sBkYA1wgaoWAe8B64BWQEPgr8DmSnaTBfQXkYY+8y4DlgYk6ACqs4lARBKAh4HrVfVTVd2tqkWqOl5V73DXiRSR50Vkg/t4XkQi3WUDRSRTRP4hIlvc2sQVPvs/VUQWisguEVkvIre78/c763Dn7TvLEZGRIvIfnzPHqSLS1H3tHSKyWER6+my7WkTudl9rh4i8LSJRIhILfAU09zmzbF72bFZEznCr6tluVbJjmX3fLiLz3DOgj0UkqoL383I31udEZDvwoDv/ShFZ5MY2UURaufPFXXeLu+95ItLF5z14VUS+cd+/H/du5y7vLyIz3e1mikh/n2U/iMgjbiy7RGSSiDRyl0WJyPsiss0t70wRabL3eBCRN93/43oReVREQssp5xDgHuAC9z39zZ3fXETGich2EVkuIldXdOxVBfeL6R2gKc6XUtk4+4pT080Rkc0i8myZVYaJyFpxzm7v9dmuwmPeXX6miGS4+13hvh9lX7uZ+/+8vbzY3ePqJPf5gyIyWkTedf9fC0QkrYLtprhPf3Pf+wt8llX0OYwUkafdsm52j6vo8vbvs00j4DtgAXCJqha7i/oAI93vimJVnauqX1Wyq0LgM+BCd7+hwPk4JwC+r1fZ8dzGPf53icg3QKMy26aLUzPMFpHfpIouuf2JqtbJBzAEKAbCKlnnYWA60BjnDOwX4BF32UB3+4eBcOBUIA+o7y7fCBzjPq8P9HKfXw78XOZ1FGjvPh8JbAV6A1E4B+QqnLOPUOBR4HufbVcD84EWQANgKvCoT4yZZV7rQeB993kHYDcwyC3DncByIMJn3zOA5u6+FwF/r+C9utx9P24EwoBoYKi7v47uvPuAX9z1TwZmA4mAuOs083kPdgHHApHAC3vfMzeOHcCl7j4vcqcbust/AFa4ZYt2px93l10DjAdi3PeyN1DPXfYZ8BoQ6/6/ZwDXVFDWfe+hz7wfcWqTUUAPnLPBEw9wDI7c+7/ymfen/1kF/7tI4ClgXQXrTgMudZ/HAenu89Y4x9sI9/3pDhQAHf045vsCO93jJQRIBo70ed+vcve/FBheSblXAyf5lCkf5/MTCvwbmF7Jtvs+K35+Dp8HxrnHTbz7//93JcfwQpwE8CogZZZPxvl8XQi0PMD/diCQCfQHfnXnnQpMdN+nH/w8nqcBz7r/72NxPhd7j4FkYJu73xD3/7INSPL9n1TJ92VV7KQmPoBhwKYDrLMCONVn+mRgtc8/eg8+iQTYwh8fuLU4Xzz1yjnYDpQIRvgsuxFY5DPdFcgu86H6u8/0qcAK34OxzGs96HMg/QsY7bMsBFgPDPTZ9yU+y58EXq3gvbocWFtm3lfA38rsPw+nan0CzhdGOhBSZruRwEc+03FACU6yuxSYUWb9acDlPgf/fT7LrgO+dp9fifPF1q3M9k1wvgyjfeZdhE/Creg9dKdbuPHF+8z7N87ZY2XH10jKTwSlQLbP43yf1y10523BOUnoXcG+pwAPAY3KzG/tHm8pPvNmABf6ccy/BjxXwev9gPOFtRq46ADlXs3+iWCyz7JOwJ5Kti0vEZT7OcQ5wdgNtPNZ1g9YVckxvAsoAo4qZ3l94HGcRFECZAB9KtjXQNzPHrAMOAL4COd7xzcRVHg841yqLgZifZZ9yB+f338C75XZdiJwmc//pEoSQZ29NISTORtJ5deym+NcI9xrjTtv3z70j2ojOF9yce7zc3C+lNe4VbuDadTzve64p5zpuP1XZ10lMVZmv/Kpaqm7r2SfdTb5PPctX3nWlZluBbzgVluzge04H85kVf0OeBl4BdgsIq+LSL3y9qWque62zcvG7FrjZ8zv4XxQPnIvezwpIuFunOHARp9YX8M5K/ZHc2C7qu6qJKaDsUFVE30eo32WjXbnNVbVE1R1dgX7+BtOrWixe7nhtDLLK3qPKjvmW+AkiooMwzmR+KSSdcpTNpaoA3wuy6roc5iEU/ub7fN//dqdX5HfgNuBr8TnEiyAqu5Q1btUtTPOyUMG8JnIAdto3gNuAI4HxpZZVtnx3BzYoaq7yyzbqxVw3t6yueUbADQ7QDwHrS4ngmk4VdKhlayzAefN3qulO++AVHWmqp6J82XyGbD3w7wb5+AEQESaHkTMFWlRQYwH6jp2v/K5B3QLnA/zoSj7eutwLq/4fqlFq+ovAKr6oqr2BjrjfGnd4bPtvjKJSBxOFXpD2ZhdLf2JWZ02oIdUtRNOlf00nEtu63BqBI184qznfuD9KecGoIGIxB9sTIGiqstU9SKc4+8J4BNx2o0OpLJjfh3QrpJtH8S5rPlhee0rHtiKc+LU2ef/mqBOY3uFVPUFnDP/b8Rttypnna3A0/xx2bQy7+HUTL9U1bwyyyo7njcC9cv831r6PF+HUyPw/XzFqurjB4jnoNXZRKCqO4H7gVdEZKiIxIhzi9gpIvKku9oo4D4RSXIbkO4HDnjboIhEiMgwEUlQp1EvB6cqCc4ZR2cR6SFOw+uDVVCc68W5ta0BTkPmx+78zUBDcRrGyzMa+IuInOieGf8D5wvxlyqICZzrrHeLSGfY1yB7nvu8j4gc5b7ubpykXOKz7akiMkBEIoBHcK6zrgO+BDqIc9tvmNtg2AmYcKBgROR4Eenqfknl4FwCKFHVjcAk4BlxbhkMEZF2InJcBbvaDLQWkRAAN65fgH+L0yDdDeeM/IMKtg84EblERJLcWl62O7uksm1clR3zbwJXuMdLiIgki8iRPtsWAefhtLO8t/f9qWKbgbb+rOiWfQTwnIg0BnBjPtmPbZ/EaZuaLCJHuNs+ISJd3OMuHrgWWK6q2w6wr1XAccC95Syu8HhW1TXALOAh9ztlAOB7B9n7wOkicrKIhLrH3kARSTlQ+Q5WnU0EAKr6LHAbTiNmFk6GvQHnDB6chtlZwDzgd2COO88flwKrRSQH+DtwifuaS3EatibjXDv8ucI9+O9DnC+yle7jUfe1FuN8sFe6Vcf9Lhmp6hI3rpdwzp5OB05X1cIqiAlVHYtzNvqR+z7MB05xF9fD+ZDuwKnubsM5w/It0wM4l4R641x2wP3QnYaTtLbhNHCf5p6hHUhTnMsWOTgN3z/yx5fcX4EInMbCHe56FVWx/+f+3SYic9znF+Fcf9+AU/1/QFW/8SOmQBkCLBCRXJwvtAtVNd+P7So85lV1BnAF8BxOo/GPlDmbdY+ds3FqIm8FIBk8CLzjHs/n+7H+P3FuWJjuHoOTca7XH5CqPgK8AXwrIu1wavJjcRLrSpyyn+Hnvn5W1T9dTfDjeL4YOArnc/AA8K7PtutwbnW/hz++v+4gAN/b4jY6mBpKRFbjNAhN9jqWqiIiI3Ea2u7zOhZjTB2vERhjjDkwSwTGGBPk7NKQMcYEOasRGGNMkKt1HYc1atRIW7du7XUYxhhTq8yePXurqpb7Y7talwhat27NrFmzvA7DGGNqFREp+wvnfezSkDHGBDlLBMYYE+QsERhjTJCrdW0Expi6oaioiMzMTPLz/ekZw/grKiqKlJQUwsPD/d7GEoExxhOZmZnEx8fTunVrDtzTs/GHqrJt2zYyMzNp06aN39vZpSFjjCfy8/Np2LChJYEqJCI0bNjwoGtZQZMIikpKmbrcnw4sjTHVxZJA1TuU9zQoEsHugmKuemcWw9/8kak/fet1OMYYU6MERSJYsy2PZatW80H4o3SZfCm/z5nmdUjGmBogLs4ZzGzDhg2ce+65HkfjnaBIBJ2a12PEsXvoEbKSBNlN43EXs3L5Yq/DMsbUEM2bN+eTTw52KOaDU1xcfOCVPBIUiQCg86DL+L3jrQA0YTshH5zNxo2eDTlrjKlBVq9eTZcuzvDFI0eO5Oyzz2bIkCGkpqZy55137ltv0qRJ9OvXj169enHeeeeRm5sLwMMPP0yfPn3o0qULw4cPZ2+vzgMHDuSee+7huOOO44UXXqj+gvkpqG4f7Xr+A8x7YxPd1o+ita5nwRtnE3PTJBISKhry1xhTHR4av4CFG3KqfL+dmtfjgdM7H/R2GRkZzJ07l8jISI444ghuvPFGoqOjefTRR5k8eTKxsbE88cQTPPvss9x///3ccMMN3H///QBceumlTJgwgdNPd4Yfzs7O5scff6zSclW1oEoEiND1b6/w+0tZdN0xmc4li5n1n3PpctsEoiIjvY7OmKC1cEMOv67a7nUY+5x44on7ThA7derEmjVryM7OZuHChRx99NEAFBYW0q9fPwC+//57nnzySfLy8ti+fTudO3felwguuOACbwpxEIIrEQASEkrnaz9g8fOncGTeHNIKZvDTy5fR/5YPCQ0NmitlxtQonZrXq1H7jfQ5MQwNDaW4uBhVZdCgQYwaNWq/dfPz87nuuuuYNWsWLVq04MEHH9zvPv7Y2NhDC74aBV0iAAiJiKLN9Z+y6vkTaVO0gr45k3ltzASuO/8Mr0MzJigdyuWb6paens7111/P8uXLad++PXl5eWRmZtK4cWMAGjVqRG5uLp988kmtuwMpaE+BI2Pr0+jv41kW2o6/Ft7Fk3NC+Xr+Jq/DMsbUUElJSYwcOZKLLrqIbt26kZ6ezuLFi0lMTOTqq6+ma9euDB06lD59+ngd6kGrdWMWp6WlaVUOTLNu225OfelnduUXUy8qjC9vPoaU+jFVtn9jTPkWLVpEx44dvQ6jTirvvRWR2aqaVt76QVsj2KtFw1ieOrcbADn5xfzr/W8pqsH3+xpjTFUL+kQAMKRLMy7r14rjQ+byzNa/M3XkvV6HZIwx1cYSgevuk9vzcNQoGkgux6x7jTk/feF1SMYYUy0sEbiioqKQc9+kQMMJFSX52xvYstl+eWyMqfssEfhI6ZTO4h53A043FOveupySklKPozLGmMCyRFBG96G3Ma/eQAB6F8zg5/ce8jYgY4wJMEsEZYmQetXbbJQmAPRb9RJLfvvV46CMMbXJ5Zdfvq8306uuuoqFCxd6HFHlLBGUI7peAwqGjqBEhQgpQT6/zgbYNsYckjfeeINOnToFbP9V0b21JYIKtO5+HBkt/wrA7mIYMWm2xxEZY6ra6tWr6dixI1dffTWdO3dm8ODB7Nmzh4yMDNLT0+nWrRtnnXUWO3bsAJxupf/5z3/St29fOnTowE8//XTA1xg4cCB7fwQbFxfHvffeS/fu3UlPT2fz5s0AZGVlcc4559CnTx/69OnD1KlTAZgxYwb9+/enZ8+e9O/fnyVLlgBOV9nnnXcep59+OoMHDz7s9yGgfQ2JyBDgBSAUeENVH69gvXOB/wF9VLXqfjZ8mLpf8jivvBjPs9uOQqdlc0zPbHq0SPQ6LGPqprkfQMaHla/TtCuc4vM1snEefH13+ev2uBh6Djvgyy5btoxRo0YxYsQIzj//fMaMGcOTTz7JSy+9xHHHHcf999/PQw89xPPPPw84Z+AzZszgyy+/5KGHHmLy5Mn+lpDdu3eTnp7OY489xp133smIESO47777uPnmm7n11lsZMGAAa9eu5eSTT2bRokUceeSRTJkyhbCwMCZPnsw999zDmDFjAJg2bRrz5s2jQYMGfr9+RQKWCEQkFHgFGARkAjNFZJyqLiyzXjxwE1DjLsSHRcZw0qV38cJLP1NYUsrt//uNCTcOICo81OvQjKl7stfCmp8Pbpv8nRVv03qAX7to06YNPXr0AKB3796sWLGC7OxsjjvuOAAuu+wyzjvvvH3rn3322fvWXb169UGFGxERwWmnnbZv+2+++QaAyZMn79eOkJOTw65du9i5cyeXXXYZy5YtQ0QoKirat86gQYOqJAlAYGsEfYHlqroSQEQ+As4EyraaPAI8CdwewFgO2RFN47n5pFSemriE5Vt2Merz8Vxx7lCvwzKm7klsCa0O8OXdtOv+01EJFW+T2NKvly3b5XR2drZf6+/tnhrgiiuuYO7cuTRv3pwvv/yywm3Dw8MRkT9tX1payrRp04iOjt5v/RtvvJHjjz+esWPHsnr1agYOHLhvWVV2bx3IRJAMrPOZzgSO8l1BRHoCLVR1gohUmAhEZDgwHKBlS//+uVXpmmPbMmfePK7Y+jR9f1/MovaN6dijf7XHYUyd1nOYX5dy9tOsG1xRtb0AJCQkUL9+fX766SeOOeYY3nvvvX21g4q8/fbbh/WagwcP5uWXX+aOO+4AnBHSevTowc6dO0lOTgacdoFACWRjsZQzb19XpyISAjwH/ONAO1LV11U1TVXTkpKSqjBE/4SFhnD/CY1JD1lEhJQQNu56u4vImDrsnXfe4Y477qBbt25kZGTsG4YyUF588UVmzZpFt27d6NSpE6+++ioAd955J3fffTdHH300JSUlAXv9gHVDLSL9gAdV9WR3+m4AVf23O50ArABy3U2aAtuBMyprMK7qbqgPxqy3biVt7VsATEm+mmOvftqTOIypC6wb6sCpSd1QzwRSRaSNiEQAFwLj9i5U1Z2q2khVW6tqa2A6B0gCXusx7P9YHdoKgH6Zb9kPzYwxdULAEoGqFgM3ABOBRcBoVV0gIg+LSK0cEzIsMhoZ+h9KVAiXEkrH3WhjFxhjar2A/qBMVb9U1Q6q2k5VH3Pn3a+q48pZd2BNrg3s1arrAOYmXwxAx5IlTB/9lMcRGVN71bYREmuDQ3lP7ZfFh6DLsMfZJM6A1T2XvMD6NSs8jsiY2icqKopt27ZZMqhCqsq2bduIioo6qO0C+sviuioqth47TniCpt9egQIfjP+KO66/ft/9wcaYA0tJSSEzM5OsrCyvQ6lToqKiSElJOahtLBEcoo7HnM24hRk8tiqVzZkN6JCxgaE9k70Oy5haIzw8nDZt2ngdhsEuDR2W4y79FyVxzQB4eMJCduwu9DgiY4w5eJYIDkNCTDgPnO50L7t9dyHPjZvmcUTGGHPwLBEcptO6NeOkDoncEDqWuxefy7xpk7wOyRhjDoolgsMkIjxyfCI3hn1GtBQSN+kf5Ofv8TosY4zxmyWCKtCsTWfmtb8GgLa6lpkf2DjHxpjawxJBFel5wb9YE+r0jNpn7RusXva7xxEZY4x/LBFUkbCIKIpOeRaAKCki+5Ob0dJSj6MyxpgDs0RQhdqnDWJ2w9MB6FEwm2kT3vQ4ImOMOTBLBFWswyXPsoN6AKTOeZQd27d6HJExxlTOEkEVi6/fmLVp9wCQRDaT//eqxxEZY0zlLBEEQLdTr+Hn2EFcWXg7d6zqya8rt3kdkjHGVMgSQQBISAit/vYuv4Q6gwHd+9l8Cout4dgYUzNZIgiQFg1iuOnEVACWb8llxBTrqtoYUzNZIgigq49pS4cmcRwlizjmxwvIXL3U65CMMeZPLBEEUHhoCM8cH8XHkY/QTVaw6eNbbBAOY0yNY4kgwLr2OIq5CYMASNszlZkTP/A4ImOM2Z8lgmrQZtjz5BALQMvpD7Bz5w6PIzLGmD9YIqgGiY1TWNHtdgCaspXf37/b44iMMeYPlgiqSY+hN7M0oiMA6Vs+ZknGVI8jMsYYhyWCaiIhoUSf/RJFGkqYlML4WyguKvI6LGOMsURQnVoc2YeMlGEAHFGylF8+fdHjiIwxxo9EICJHi0is+/wSEXlWRFoFPrS6qevFj5EpzXi9+C/ctqAd67NtNDNjjLf8qRH8F8gTke7AncAa4N2ARlWHRcXWI/OCyfxf8TC2Fobz4LgFXodkjAly/iSCYnV+BXUm8IKqvgDEBzasui39yBTO6pkMwDcLNzNxwSaPIzLGBDN/EsEuEbkbuAT4QkRCgfDAhlX33fuXjiREh5PILraOuYNdOdleh2SMCVL+JIILgALgb6q6CUgGngpoVEGgUVwk/3dMFJMj72BY6Tjm2W8LjDEe8atGgHNJ6CcR6QD0AEYFNqzgcMqx/dke0RyA9M2jWDznJ48jMsYEI38SwRQgUkSSgW+BK4CRgQwqWISEhRF9zssUaSihooR8cQtFRYVeh2WMCTL+JAJR1TzgbOAlVT0L6BzYsIJHiyP7kNHyrwB0KFnOrx/92+OIjDHBxq9EICL9gGHAF+680MCFFHy6X/wY60OaAdBr+StkrlzscUTGmGDiTyK4GbgbGKuqC0SkLfB9YMMKLhHRseQOehqAGClg6+gb0FIb2tIYUz0OmAhUdYqqnqGqT7jTK1X1psCHFlyO6Hcas+ufCkCP/Jn8OuENjyMyxgQL62uoBkm99Hm2U4+M0rY8Oxe25RZ4HZIxJggENBGIyBARWSIiy0XkrnKW/11EfheRDBH5WUQ6BTKemq5egybMH/wRZxc+zIw9yTwyYaHXIRljgkDAEoH7C+RXgFOATsBF5XzRf6iqXVW1B/Ak8Gyg4qktjunXnxM6Og3Hn2Vs4PvFWzyOyBhT1/nT+2gbt8fRT0Vk3N6HH/vuCyx32xQKgY9w+ivaR1VzfCZjgaAf2V1EeGRoZ+IjwwDlh09eZleODW1pjAmcMD/W+Qx4ExgPHMytLMnAOp/pTOCosiuJyPXAbUAEcEJ5OxKR4cBwgJYtWx5ECLVTs4Ro7h+UTJOJf+fY4t/55d319L/hTa/DMsbUUf5cGspX1RdV9XtV/XHvw4/tpJx5fzrjV9VXVLUd8E/gvvJ2pKqvq2qaqqYlJSX58dK137n9O9I42sm76VljmD9toscRGWPqKn8SwQsi8oCI9BORXnsffmyXCbTwmU4BNlSy/kfAUD/2GxQkJJR6579GvoYTIkq9SbewZ3eu12EZY+ogfxJBV+Bq4HHgGffxtB/bzQRS3TaGCOBCYL+2BRFJ9Zn8C7DMn6CDRfP2XZmXej0ALXUDc977041Xxhhz2PxJBGcBbVX1OFU93n2Uey3fl6oWAzcAE4FFwGj3l8kPi8gZ7mo3iMgCEcnAaSe47BDLUWf1vvA+loc5+fKojR+weI4/V+WMMcZ//jQW/wYkAgd9H6Oqfgl8WWbe/T7Pbz7YfQab0LBwIs75D4UfDSFCSoiYcCMFnX8lMjLa69CMMXWEPzWCJsBiEZl4kLePmirSsmNfMlpfBUDb0jXMfv9fHkdkjKlL/KkRPBDwKMwB9Rz2MKuemEij4s2MXyXUW7+TLskJXodljKkD/Ol07kdgMc6A9fHAIj9vHzVVKDwiipKzRnBq0VOMKh7IbaMzKCgu8TosY0wd4M8vi88HZgDnAecDv4rIuYEOzPxZ+y59OfeEdACWbs7l2W+WehyRMaYu8KeN4F6gj6pepqp/xek6wi5Se+S649vRLcW5JPTDTz+y0MY5NsYcJn8SQYiq+t4xtM3P7UwAhIeG8Mx53Rke/hXjwu8ldvw15O3e5XVYxphazJ8v9K/dO4YuF5HLcYar/PIA25gASm0Sz0kd6hMpxbTS9WS8favXIRljajF/GovvAF4DugHdgddV9Z+BDsxUrvdFD7AkvCMA/bf+j3k/2R29xphDU2kiEJFQEZmsqp+q6m2qequqjq2u4EzFQsPCiL/wDfZoBABJ397GzuztHkdljKmNKk0EqloC5ImI3bBeAzVv14X5nf8BQDOyWDzyBo8jMsbURn51Qw38LiJvisiLex+BDsz4J+3cO5gf2ROAo7K/YPakDz2OyBhT2/iTCL7AuV10CjDb52FqAAkJJemSEezC6XuozS93smX9Go+jMsbUJhUmAhH51n3aSVXfKfuopviMH5q0SGVZn4cB+LGkG/d8sZzS0qAf9dMY46fK+hpqJiLHAWeIyEeUGXFMVecENDJzUHr9ZTgvZ0Xz9OIGsDKfET+t5Jrj2nkdljGmFhDV8s8c3W4k/gYMAGaVWaz+jEkQCGlpaTprVtlwDEBuQTF/efEn1mzLIzxUGHvd0dYxnTEGABGZrapp5S2r8NKQqn6iqqcAT/oMSOP3wDSm+sVFhvHChT0JCxFCS/KZPfJO8nbneB2WMaaG8+cHZY9URyCmavRokci9x9ZnfMR9XFb0EfPetFtKjTGVsz6D6qC/ntSH4qj6AKRv/5zZE9/3OCJjTE1miaAOCg0Lo8ElI8khBoC20+5i8/pVHkdljKmp/EoEblcTzUWk5d5HoAMzh6dJy1SWH/UYAPXZRdY7f6W4qNDjqIwxNZE/A9PcCGwGvsH5cdkXwIQAx2WqQK9TrmRW/VMB6FI4jxkj7/Q4ImNMTeRPjeBm4AhV7ayqXd1Ht0AHZqpGl6tfZ1VIKwD6r3+b377/xOOIjDE1jT+JYB2wM9CBmMCIiokn7MJ3ydNIAJr9eDsbtu7wOCpjTE3iTyJYCfwgIneLyG17H4EOzFSdFh16sLjPo2RqI64puIXrRy+ksLjU67CMMTWEP4lgLU77QAQQ7/MwtUiv04bzZrdRzNVU5q7N5smvF3sdkjGmhqisryEAVPUhABGJdyY1N+BRmYD45xm9mbnhF+avz+GNn1fRt1U8g7u28DosY4zH/LlrqIuIzAXmAwtEZCn/5vAAAB1lSURBVLaIdA58aKaqRYWH8srFvYiPDGNQyCw6jTmB9SutZmBMsPPn0tDrwG2q2kpVWwH/AEYENiwTKK0axvLfwZGMiHiWFLaQ98Ew8vOskmdMMPMnEcSq6vd7J1T1ByA2YBGZgBtw9EB+bXw+AKkly/n9tSvQUms8NiZY+XXXkIj8S0Rau4/7AOuvoJbrddXLLIzoCkCfnZOYPur/PI7IGOMVfxLBlUAS8Ckw1n1+RSCDMoEXHhFJk799xGYaAtBn6TPM/3mcx1EZY7zgTzfUO1T1JlXtpao9VfVmVbVfJNUBDZukkDP0HfI1nDApJXnydWxYvcTrsIwx1ayyMYufd/+OF5FxZR/VF6IJpNQexzC/tzPecX12see9C2wwG2OCTGW/I3jP/ft0dQRivJN2xnVMX/8b6Zs/YmNhDK9+OpcnLzkWETnwxsaYWq+yoSpnu097qOqPvg+gR/WEZ6pL2lUv8VbCDVxWdBf/W5DLa1NWeh2SMaaa+NNYfFk58y6v4jiMx8LCIxg6/AGa1Y8D4ImvFzNpwSaPozLGVIfK2gguEpHxQJsy7QPfA9v82bmIDBGRJSKyXETuKmf5bSKyUETmici3ItLq0ItiDleD2AhevzSNmIhQREtZ9/HtLMv4yeuwjDEBVlkbwS/ARqAR8IzP/F3AvAPtWERCgVeAQUAmMFNExqnqQp/V5gJpqponItcCTwIXHFwRTFXq1LweL13Yg9JRFzMoZDZZn/3MpoaTadqivdehGWMCpLI2gjWq+oOq9ivTRjBHVYv92HdfYLmqrlTVQuAj4Mwyr/G9qua5k9OBlEMtiKk6J3ZqSnynkwBIYgd5b59Dbo7dMWxMXeVPp3PpIjJTRHJFpFBESkTEn/sLk3EGtdkr051Xkb8BX/mxX1MN0i+6h1+TzgWgbelqVvznPBvz2Jg6yp/G4peBi4BlQDRwFfCSH9uVd++hlruiyCVAGvBUBcuHi8gsEZmVlZXlx0ubqpB2zWtkRKcD0D1/JrNfu8b6JDKmDvInEaCqy4FQVS1R1beB4/3YLBPw7ew+BdhQdiUROQm4FzhDVQsqeP3XVTVNVdOSkpL8CdlUgdCwMFKv+5gVoW0BOGrrp/z60WMeR2WMqWr+JII8EYkAMkTkSRG5Ff96H50JpIpIG3f7C4H9fpEsIj2B13CSwJaDjN1Ug9j4ROKuHMMWGgDQd8kzzPnqLY+jMsZUJX8SwaVAKHADsBvnLP+cA23kNijfAEwEFgGjVXWBiDwsIme4qz0FxAH/E5EM67qiZmqS3Jacsz9gt0YRIsoXv2QwdflWr8MyxlQRUS33sn2NlZaWprNmzfI6jKD0+5TPeWfSdD4pHkBsRCijhqfTLSXR67CMMX4Qkdmqmlbessp+UDba/fu7+4Ov/R6BCtbUXF2PPZPjz78JEdhdWMLlb89k+RYb3cyY2q6yH5Td7P49rToCMbXDX7o1Y0deF+77bD5huzez5dXTib9yBE1S7AdnxtRWFSYCVd3oPj0b5/r++uoJydR0l6S3Ynd2Fqf+cjMtSrNY89aZZF87mcSkZl6HZow5BP40FtcDJonITyJyvYg0CXRQpuYbPrgXmU0GAtCqNJMtr53Bbvv1sTG1kj8jlD2kqp2B64HmwI8iMjngkZkaTUJCOOrvrzGz3iAAOhQvZc3Lp7Mn1wa1Maa28esHZa4twCacnkcbByYcU5uEhIbS/foPyIg+CoBOhb+z8qXTyM/b5XFkxpiD4U9fQ9eKyA/Atzg9kV6tqt0CHZipHSIiIznyprH8FuXclda54DeWv3A6+Xl2N5ExtYU/NYJWwC2q2llVHyjTjbQxREXH0uHGz5kX2QuALgVz+e3liykoLvE4MmOMP/xpI7gLiBORKwBEJElE2gQ8MlOrRMfG0f6mz5kf0YNsjeXhHYO59v05lgyMqQX8uTT0APBP4G53VjjwfiCDMrVTTGw92tw0noeSnmOBtuG7xVu4/oO5FBZbj6XG1GT+XBo6CzgDp58hVHUDEB/IoEztFRtXj0euPofereoDMHnRZp5+411rMzCmBvMnERSq0yGRAoiIPz2PmiAWFxnGyCv60LNlIv1CFvCPjbez8vlTyM3Z7nVoxphy+JMIRovIa0CiiFwNTAZGBDYsU9vFR4Xz7pV9uS3+OyKliE6F89j44mB2btvkdWjGmDL8aSx+GvgEGAMcAdyvqv6MUGaCXHxUOF1vGcNcd5Sz1OJl7HhlEFs3rvE4MmOML39HKPtGVe9Q1dtV9ZtAB2XqjqjoWDrfMo6Z8ScC0Lp0LQWvD2bTmiUeR2aM2auybqh3iUhORY/qDNLUbhGRkfS6eTTT658JQLJuIuTtIaxbmuFxZMYYqCQRqGq8qtYDngfuApJxxh3+J/Bo9YRn6orQsDCOunEkvzQZBkBjthP/4V9YMHeax5EZY/y5NHSyqv5HVXepao6q/hc/hqo0piwJCaHfNS8ztdW1AKwubcLFYzbz9XxrQDbGS/4kghIRGSYioSISIiLDAPu5qDkkEhLC0Vc8ztTuTzC8+A52Fodz7QezeevnVV6HZkzQ8icRXAycD2x2H+e584w5ZEef9XeeuOxEYiJCUYWHJyzg03eeo6S42OvQjAk6/tw+ulpVz1TVRqqapKpDVXV1NcRm6rjjj2jM6Gv6kRQfyY2hYzl71YPMf/Z08nfbvQjGVKeDGY/AmCrXJTmBscN7MTRyFgDd835h3XMnsHW9XSoyprpYIjCeS0lqQKObvtvXjXVq8TIYMZCls2wgPGOqgyUCUyMkJDbkyNu+ZloD57cGjcim9fjzmf3p8x5HZkzd53ciEJF0EflORKaKyNBABmWCU0RkJOk3vsO0jvdRqKFESAm95z3AzFeuoLgw3+vwjKmzKvtlcdMys27D6Y56CPBIIIMywUtE6HfBHSwd8iHbSACgT9anPP/aq+zYXehxdMbUTZXVCF4VkX+JSJQ7nY1z2+gFgN3WYQKqS78hFFzxLUtD2/N28cm8vD6VM175md8zd3odmjF1TmVdTAwFMoAJInIpcAtQCsQAdmnIBFzzVqmk3PYDczreDsC67Xs457+/MPr7mWipjXpmTFWptI1AVccDJwOJwKfAElV9UVWzqiM4Y2Ji43nx4j7cfcqRhIYIUSW7SP/+IuY9fSo527d4HZ4xdUJlbQRniMjPwHfAfOBC4CwRGSUi7aorQGNEhGuOa8foa9L5V8yntAzJonveNPa81I9ls771Ojxjar3KagSP4tQGzgGeUNVsVb0NuB94rDqCM8ZX71YNGHT9S8yOGQBAE91Km/HnMuP9+9FS6/7KmENVWSLYiVMLuBDYVwdX1WWqemGgAzOmPIkNk+h1+3h+Sb2TQg0jTErpu/wFFjx5ElvXr/A6PGNqpcoSwVk4DcPFWCdzpgaRkBD6D7uXFWeOZb00AaBL/hwiRwxg7oTXQdXjCI2pXSq7a2irqr6kqq+qqt0uamqcjr2OJe7m6UxPOBWAePKInvEi//hoJjvzijyOzpjaw7qYMLVaQmID0m8dxax+r7CJhtxWdC1jfsvi5Oen8POyrV6HZ0ytYInA1AlpJ19CyM0ZNO7QB4BNOflc8uZ0vnz9PnKy7W5nYypjicDUGY3r1+Pty/vw2FldiA4P5cyQqZy64SWKnk8j4+u3rO3AmAoENBGIyBARWSIiy0XkrnKWHysic0SkWETODWQsJjiICMOOasVXNx/DuQlLAGhINj2m38q8p4aQZXcWGfMnAUsEIhIKvAKcAnQCLhKRTmVWWwtcDnwYqDhMcGrdKJYBd3zC9B6Ps514ALrlTSdmRH9mfvxvSm1ITGP2CWSNoC+wXFVXqmoh8BFwpu8K7jCY83D6MDKmSklICOlDr6X02hn8Wm8wALHk02fR46x4vB8r5v7gbYDG1BCBTATJwDqf6Ux33kETkeEiMktEZmVlWcOfOTiNmjTnqNv+x9yBb7Me53cHqcVLafTZRTzwv1/ZllvgcYTGeCuQiUDKmXdIrXWq+rqqpqlqWlJS0mGGZYJVz4Fnk3j7LKY2u4xCDeOF4nN4Z/ZWjn/6B0ZOXUVxiVVMTXAKZCLIBFr4TKcAGwL4esYcUGxcPY6+5kU2XDKFVW2dH8zn5Bfz4PiF/Pepu1k4ZYzdXWSCTiATwUwgVUTaiEgETp9F4wL4esb4rXVqZ966sh9v/DWNlg1iaCGbGb7nTTp9dyULnjieVfN+9jpEY6pNwBKBqhYDNwATgUXAaFVdICIPi8gZACLSR0QygfOA10RkQaDiMaYsEeGkTk2YdOux3NVLKSIMgM75c2nz6V+Y++xZbFy10OMojQk80VpWDU5LS9NZs2Z5HYapg7ZsWsfyTx4kLWssEeJ0a12koWQ0OYvUcx8msfEh3etgTI0gIrNVNa28ZfbLYmNcjZu2oP8Nb7LhkinMiDsBgHApoc+WT4h4pScT33/aOrMzdZIlAmPKaJ3ahb63j2XRGROYF9ETgBgpYOTCUgY88R3PTFpCdl6hx1EaU3Xs0pAxlVBVMn4cy7rpY7kp+wL23hUdFxnKC22m0/vUq+2SkakV7NKQMYdIROg58GxO/+e7vHV5H7qnJADQq2gOJ65+jshXejDzP1exec1ijyM15tBZjcCYg6Cq/LA0i92f/YPT9vxxN3SJCvMSjifxpNto0+0YDyM0pnyV1QgsERhzCLS0lHlTJ8DPz9G9YM5+yxZGdkf73UinY89BQqzSbWoGSwTGBNCSjKns/PZZeuV8R5j80U3Ff6Oupt7xNzK0RzKxkWEeRmiMJQJjqsX61UtZ88XTdN/yOREU0b/gRbKoT3xkGOempXBZt1hat2rtdZgmSFkiMKYaZW/LYup343lseSs27MwHIJ48pkdez5qoIynseTmdj7+I8MhojyM1wcQSgTEeKC4p5dvFW3h32mpSV33Ag+Hv7luWTTzLmpxKk4FX07JjH++CNEHDEoExHlu9bD4bvnmJzpvHkyC791u2LKwDOUdewJGDriQ2oYFHEZq6zhKBMTVE3u5d/P7th0TP/5BuhRn7LftOezOu4zOc2SOZAamNCA+1O45M1bFEYEwNtHbFQtZ9N4L26z+nCdu4ufA6Pi8dAECD2Aiub7uZAe0b0KHPyUhIqMfRmtquskRg97QZ45GW7TrRst1zFBc9yZyfPid0QzMiF+dQUFzK9t2FdFnyEkcsW8zmrxuypvFJJPQ+h9ReJxISZh9bU7WsRmBMDZJbUMzE+Zv4ds5insu8gEgp3m/5NhJZ2eh44nqeTYejTiE0LNyjSE1tY5eGjKmFtm7ewNLv36Peyi/oWDCPUNn/s5pNPCPbvUjHXv05JrURMRFWUzAVs0RgTC2XtWkdy6eMJmbFF3TKzyBcStijEfQoeJ0CIogIC6Ff24ZcU+8X2vYZQtNWR3odsqlhLBEYU4ds37qZpVNGk7l2JfduHUxBsdOtRUvZzJTIWwFYF5LMxkb9iT7yJNr3PYXouAQvQzY1gCUCY+qoPYUlTF2+lW8Xb6be/Pe5u/S1P61TqKGsiOxMTsqxNOp2Mm26DiDEbk0NOpYIjAkCpSWlrJg/gy1zxpGw4Sc6FC7YN/byXhu1AUPkVY5q25D0tg3p1yaRI5rE251IQcBuHzUmCISEhpDaPZ3U7ukA7Mzezu8zJlKwdDLJ26bRStczvbQjO4uKmbRwM5MWbqaPLObNyGdYFdONgmZ9SDjiGNp0G0BEVIzHpTHVyWoExgSJzFVLyFi9hW82xzFtxTa27CrgptBPuS38k/3WK9QwVkWkkt2oF1Ft+9Oi67E0aNrSo6hNVbFLQ8aY/agqq7flsXr658Qu/ZSUnN9ozpZy151V2oFb456ge0oiPVok0rNZJJ2axlkDdC1jl4aMMfsREdo0iqXNaRcDFwOwMXMlmb99R/Gq6TTcMZe2xSsJk1IyStuxbvse1m3fw4R5Gxka8jPPhP+XNaEpZMV3pKRxV+q1TaNFp3TirNO8WskSgTEGgGYpbWmW0ha4CoBdOTtYPW8qMdkRnLStARnrstmaW0D3kBWEitKqdB2tdq6DnZNgGTAR1ktTtsR2ILtpf0p6XckRTeNJqR+NiHhaNlM5SwTGmHLF16tP1wGn0RWnzqCqbNyZz9o5xfyyrCmx2xfQomApDcjZt02ybiI5dxMTlhRww/xuzn4iwzit4XrO5ju00RHEpnSmSbvuNGzWxsZ0riEsERhj/CIiNE+MpvkJZ8IJZwKgpaVs3LCajYumk792LtFb59M4fwWLSlvt225XQTFxm2fSJ3w8bB8PS4HvIJdoNoS1ICeuLSUN2hOW3J2ErqfQskEsEWGWIKqTJQJjzCGTkBCfS0oX75s/PK+Q4zbnsmRTDos27aLN8lB25sbuNyhPHHvoULwUspdCNkxf3pGTvoklRKBFgxiOTtjK0KKv0PqtiWqSSmLyETRtfQSRUbEelLRus0RgjKlyCTER9G3TgL5t9jYev4yWvkjW5kw2Lc8gN3MBsnUx8TnLaVKUSSOyWVnaFIBShTXb8uixYw59I8ZAFk4tAihVYbM0YHtEM3ZHJ1OQ2I5N3a4npX40LRrE0KReFKEh1h5xsCwRGGOqhYSEkNSsJUnNWgJn7Lds5/YsumTt4OncGFZtzWVl1m7arYfcvGjiZM++9UJEacI2mhRug8L5LN/RnGFLBuxb3iZkC6MiHiU7vDG7o5pSFJdMSEIykQ1bEpvUkvrNWlG/UTIhoTbQjy9LBMYYzyU0SKJbgyS67Te3N1r6GFlZ68las4TcjcsozlpOeM4a4vasp0HRJtZq4/22aEoWTdlK06KtULQQdgEb93+tIg3l5Oj3SEioT5OEKBrHR3JS9idExDckukEycY2Sqd+4BfUaNA6akeEsERhjaiwJCSGpSQuSmrQATvrT8oTCIiZnF5C5I491O/ZQsr6YmWsGE7NnI4nFW2hcupXwMv0t5RPBsmwgewcAkRTyQNSzf9p3kYaSLfXICa1PXngDvky5GW3YgUZxETSIjaDVnoUkxEQR17ApiY2aERUTH4i3oFpYIjDG1FpREeG0bxxO+8Zx7pxWwJn7lpcUF7Np01qyN69hd9Y6CrdnkrcnjzNimrMpJ58tOfmE79pW7r7DpYQkdpBUsgNKVnLrgo0s1z9qCBMj7qR9SOa+6TyNJEfiyQ1NID+sHgUR9fm9yZnsaNKfxJhwEmPCaVa4hkTZQ0xiI2ITGhKfmEREZFQg3pqDYonAGFNnhYaF0TSlLU1T2u43/8Qy6+XuOoPtm9aSk7We/O2ZFO3cBLlbCN2zlYj8rcQWbUfiGhO2WygudbrlaSA5++0jRgqIoQBKtkIJUAAfbuvAmPlJ+9Z5KuxV0sOm7LfdHo1gl8SRFxJLfmg88+IG8EuTi4mPCic+KoyWJetos3suxfXbUdTyGAYesf/lsKpgicAYE/Ti4hOJi0+E1G4VrvMNzo/qcvYUs213AVtXjWRd9kaKdmVRumsL7NlBWP52wguziSraSVzJTnZFNIT8P/bhe/vsXtFSSDTboXQ7lMLMrOZ8tnHDvuWXhH7DheFv83VJH+4MjWPegydXZdEBSwTGGOM3ESEhJpyEmHBIKluv+LPXgeKSUnLyi9m5p4j89Un8tn0tRbnbKM7djuZnQ/5OQgt2Ela0i4iiXeyKbkcLiWZXfjG78oupRx4AuzSa+KjwwJQrkL2PisgQ4AUgFHhDVR8vszwSeBfoDWwDLlDV1ZXt03ofNcYEC1Vlz548dmdvJbewlPzIhnRsVu+Q9uVJ76MiEgq8AgwCMoGZIjJOVRf6rPY3YIeqtheRC4EngAsCFZMxxtQmIkJMTCwxMbEkHXj1QxbIDj36AstVdaWqFgIf4duc7zgTeMd9/glwolg3hcYYU60CmQiSgXU+05nuvHLXUdViYCfQsOyORGS4iMwSkVlZWVkBCtcYY4JTIBNBeWf2ZRsk/FkHVX1dVdNUNS0pKZAVJGOMCT6BTASZQAuf6RRgQ0XriEgYkABsD2BMxhhjyghkIpgJpIpIGxGJAC4ExpVZZxxwmfv8XOA7rW2DKBtjTC0XsLuGVLVYRG4AJuLcPvqWqi4QkYeBWao6DngTeE9EluPUBC4MVDzGGGPKF9AflKnql8CXZebd7/M8HzgvkDEYY4ypXEB/UBYIIpIFrDnEzRsBW6swnNoiWMsNwVt2K3dw8afcrVS13Lttal0iOBwiMquiX9bVZcFabgjeslu5g8vhlttGiDbGmCBnicAYY4JcsCWC170OwCPBWm4I3rJbuYPLYZU7qNoIjDHG/Fmw1QiMMcaUYYnAGGOCXNAkAhEZIiJLRGS5iNzldTyBIiJvicgWEZnvM6+BiHwjIsvcv/W9jDEQRKSFiHwvIotEZIGI3OzOr9NlF5EoEZkhIr+55X7Ind9GRH51y/2x281LnSMioSIyV0QmuNN1vtwislpEfheRDBGZ5c47rOM8KBKBzyA5pwCdgItEpJO3UQXMSGBImXl3Ad+qairwrTtd1xQD/1DVjkA6cL37P67rZS8ATlDV7kAPYIiIpOMM8vScW+4dOINA1UU3A4t8poOl3Merag+f3w4c1nEeFIkA/wbJqRNUdQp/7sHVdwCgd4Ch1RpUNVDVjao6x32+C+fLIZk6XnZ15LqT4e5DgRNwBnuCOlhuABFJAf4CvOFOC0FQ7goc1nEeLInAn0Fy6rImqroRnC9MoLHH8QSUiLQGegK/EgRldy+PZABbgG+AFUC2O9gT1N3j/XngTqDUnW5IcJRbgUkiMltEhrvzDus4D2inczWIXwPgmNpPROKAMcAtqpoTDCOfqmoJ0ENEEoGxQMfyVqveqAJLRE4DtqjqbBEZuHd2OavWqXK7jlbVDSLSGPhGRBYf7g6DpUbgzyA5ddlmEWkG4P7d4nE8ASEi4ThJ4ANV/dSdHRRlB1DVbOAHnDaSRHewJ6ibx/vRwBkishrnUu8JODWEul5uVHWD+3cLTuLvy2Ee58GSCPwZJKcu8x0A6DLgcw9jCQj3+vCbwCJVfdZnUZ0uu4gkuTUBRCQaOAmnfeR7nMGeoA6WW1XvVtUUVW2N83n+TlWHUcfLLSKxIhK/9zkwGJjPYR7nQfPLYhE5FeeMYe8gOY95HFJAiMgoYCBOt7SbgQeAz4DRQEtgLXCeqtapIUFFZADwE/A7f1wzvgennaDOll1EuuE0DobinNiNVtWHRaQtzplyA2AucImqFngXaeC4l4ZuV9XT6nq53fKNdSfDgA9V9TERachhHOdBkwiMMcaUL1guDRljjKmAJQJjjAlylgiMMSbIWSIwxpggZ4nAGGOCnCUCY6qYiCSKyHVex2GMvywRGFOF3J5uE4GDSgTisM+j8YQdeCaoici97jgVk0VklIjcLiI/iEiau7yR240BItJaRH4SkTnuo787f6A7FsKHOD9oexxo5/YX/5S7zh0iMlNE5vmMGdDaHT/hP8AcoIWIjBSR+W5/87dW/ztiglGwdDpnzJ+ISG+c7gl64nwW5gCzK9lkCzBIVfNFJBUYBeztD74v0EVVV7m9n3ZR1R7u6wwGUt11BBgnIsfi/AL0COAKVb3OjSdZVbu42yVWZXmNqYglAhPMjgHGqmoegIgcqP+pcOBlEekBlAAdfJbNUNVVFWw32H3MdafjcBLDWmCNqk53568E2orIS8AXwKSDLI8xh8QSgQl25fWxUswfl02jfObfitN/U3d3eb7Pst2VvIYA/1bV1/ab6dQc9m2nqjtEpDtwMnA9cD5wpT+FMOZwWBuBCWZTgLNEJNrt0fF0d/5qoLf7/Fyf9ROAjapaClyK09FbeXYB8T7TE4Er3bESEJFkty/5/YhIIyBEVccA/wJ6HVKpjDlIViMwQUtV54jIx0AGsAan91KAp4HRInIp8J3PJv8BxojIeTjdHZdbC1DVbSIyVUTmA1+p6h0i0hGY5g6UkwtcgnN5yVcy8LbP3UN3H3YhjfGD9T5qjEtEHgRyVfVpr2MxpjrZpSFjjAlyViMwxpggZzUCY4wJcpYIjDEmyFkiMMaYIGeJwBhjgpwlAmOMCXL/DxDvM/rk/2LeAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "ks_td = td_solve(ss=ks_ss, block_list=ks_blocks,\n", + " unknowns=['K'], targets=['asset_mkt'],\n", + " Z=ks_ss['Z'] + 0.01 * 0.8 ** np.arange(300))\n", + "dOut_ks_nonlin = 100 * (ks_td[\"C\"]/ks_ss[\"C\"] - 1)\n", + "\n", + "plt.plot(100 * dOut_ks[\"C\"][:50]/ks_ss[\"C\"], linewidth=2.5, label=\"linear\")\n", + "plt.plot(dOut_ks_nonlin[:50], linewidth=2.5, linestyle=\"--\", label=\"non-linear\")\n", + "plt.title(r'Consumption response to TFP shock in the KS Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### One Asset HANK Example" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "On iteration 0\n", + " max error for nkpc_res is 8.25E-04\n", + " max error for asset_mkt is 2.19E-02\n", + " max error for labor_mkt is 8.55E-03\n", + "On iteration 1\n", + " max error for nkpc_res is 2.29E-06\n", + " max error for asset_mkt is 4.13E-04\n", + " max error for labor_mkt is 8.89E-05\n", + "On iteration 2\n", + " max error for nkpc_res is 1.29E-08\n", + " max error for asset_mkt is 1.30E-05\n", + " max error for labor_mkt is 9.45E-07\n", + "On iteration 3\n", + " max error for nkpc_res is 3.04E-09\n", + " max error for asset_mkt is 4.51E-07\n", + " max error for labor_mkt is 3.11E-08\n", + "On iteration 4\n", + " max error for nkpc_res is 1.11E-10\n", + " max error for asset_mkt is 1.48E-08\n", + " max error for labor_mkt is 1.68E-09\n", + "On iteration 5\n", + " max error for nkpc_res is 3.46E-12\n", + " max error for asset_mkt is 4.23E-10\n", + " max error for labor_mkt is 6.60E-11\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "hank_td = td_solve(ss=hank_ss, block_list=hank_blocks,\n", + " unknowns=['pi', 'w', 'Y'], targets=['nkpc_res', 'asset_mkt', 'labor_mkt'],\n", + " Z=hank_ss['Z'] + 0.01 * 0.8 ** np.arange(300))\n", + "dOut_hank_nonlin = 100 * (hank_td[\"C\"]/hank_ss[\"C\"] - 1)\n", + "\n", + "plt.plot(100 * dOut_hank[\"C\"][:50]/hank_ss[\"C\"], linewidth=2.5, label=\"linear\")\n", + "plt.plot(dOut_hank_nonlin[:50], linewidth=2.5, linestyle=\"--\", label=\"non-linear\")\n", + "plt.title(r'Consumption response to TFP shock in the One Asset HANK Model')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.legend()\n", + "plt.show()" + ] } ], "metadata": { From a9405368d52ed705aa29ddc4ef36c1aea7d24940 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 14:12:53 -0600 Subject: [PATCH 005/288] Update rbc.ipynb by removing excess steady state teaching content --- notebooks/rbc.ipynb | 177 ++------------------------------------------ 1 file changed, 6 insertions(+), 171 deletions(-) diff --git a/notebooks/rbc.ipynb b/notebooks/rbc.ipynb index 445a4fa..d45c357 100644 --- a/notebooks/rbc.ipynb +++ b/notebooks/rbc.ipynb @@ -139,7 +139,7 @@ "def mkt_clearing(r, C, Y, I, K, L, w, eis, beta):\n", " goods_mkt = Y - C - I\n", " euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis)\n", - " walras = C + K - (1 + r) * K(-1) - w * L # we can the check dynamic version too\n", + " walras = C + K - (1 + r) * K(-1) - w * L\n", " return goods_mkt, euler, walras" ] }, @@ -156,18 +156,14 @@ "source": [ "## 2 Steady state\n", "\n", - "The next step of solving a model is to compute its steady state. The sequence-jacobian toolkit provides functionality for computing a model's steady state from its DAG representation, but if the user already has a pre-computed steady state they can supply this in the format of a dict(ionary) mapping parameters and variable names to their values.\n", - "\n", - "We will describe an additional, enhanced framework for steady state calibration at the end of this notebook." + "The next step of solving a model is to compute its steady state. The sequence-jacobian toolkit provides functionality for computing a model's steady state from its DAG representation, but if the user already has a pre-computed steady state they can supply this in the format of a dict(ionary) mapping parameters and variable names to their values." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.1 Steady state with a standard DAG\n", - "\n", - "In the first case, we will use almost exactly the same arrangement of the DAG, with two unknowns and two targets to compute the steady state, providing an initial set of fixed variables/parameters.\n", + "We will use almost exactly the same arrangement of the DAG, with two unknowns and two targets to compute the steady state, providing an initial set of fixed variables/parameters.\n", "\n", "The one difference is that we will use $\\varphi$ (`vphi`) as an unknown instead of $L$ because we want to calibrate the steady state aggregate labor supply $L=1$. Verifying that it is a valid unknown, observe that $\\varphi$ influences the $euler$ target indirectly through $L$ mapping into $r$ and the $goods\\_mkt$ target indirectly through $L$ mapping into $C$.\n", "\n", @@ -213,8 +209,8 @@ " 'beta': 0.9900990099009901,\n", " 'vphi': 0.965891472871577,\n", " 'K': 3.6206932399091794,\n", - " 'w': 1.0253144949496455,\n", " 'Y': 1.1520387583703882,\n", + " 'w': 1.0253144949496455,\n", " 'C': 1.0615214273518796,\n", " 'I': 0.0905173309977294,\n", " 'goods_mkt': 2.0779156173489355e-11,\n", @@ -245,7 +241,7 @@ "outputs": [], "source": [ "# Solving for the steady state as a standard DAG\n", - "calibration = {\"L\": 1., \"Z\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11, \"beta\": 1/(1 + 0.01)}\n", + "calibration = {\"L\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11, \"beta\": 1/(1 + 0.01)}\n", "blocks = [household, firm, mkt_clearing]\n", "unknowns_ss = {\"vphi\": 0.9, \"K\": 2., \"Z\": 1.}\n", "targets_ss = {\"euler\": 0., \"goods_mkt\": 0., \"Y\": 1.}\n", @@ -270,8 +266,8 @@ " 'beta': 0.9900990099009901,\n", " 'vphi': 0.9658914729252107,\n", " 'K': 3.142857142122498,\n", - " 'w': 0.8900000000291391,\n", " 'Y': 1.0000000000327407,\n", + " 'w': 0.8900000000291391,\n", " 'C': 0.9214285714043695,\n", " 'I': 0.07857142855306254,\n", " 'goods_mkt': 7.530864820637362e-11,\n", @@ -500,167 +496,6 @@ "source": [ "For those of you familiar with Dynare, these impulse responses are identical to what you could obtain by running the perfect foresight solver `simul`." ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Appendix" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### A.1 Steady state with an analytical solution-augmented DAG\n", - "\n", - "In this alternative way of solving for the steady state we will make use of a new kind of block called a `HelperBlock`, whose purpose is to provide a more flexible way of using the sequence-jacobian toolkit to calibrate a model's steady state. \n", - "\n", - "A `HelperBlock` works identically to the `SimpleBlock`s that we constructed above using the decorator `@simple` for the end-user, but using the decorator `@helper` instead. Under the hood the sequence-jacobian toolkit handles them differently when the blocks are sorted/used outside of the steady state, like in the computation of the general equilibrium Jacobian which we will get to later. \n", - "\n", - "Recall in the first case, we wanted to set up the DAG to normalize $Y = 1$. Choosing $Z$ as an additional unknown and targeting $Y = 1$ turned out to be an easy fix, but in general there may be certain variables we would like to calibrate that are not so straightforward to work with. An alternate route to achieve this would be to use a `HelperBlock` to solve out a portion of the DAG analytically for the purposes of steady state calibration.\n", - "\n", - "In the case of the RBC model, given our choice of fixed $r = 0.01$, normalizing to $Y = 1$ lets us provide a *complete* analytical characterization of the steady state. In steps: we choose the discount rate $\\beta$ to hit a given real interest rate $r$, the disutility of labor $\\varphi$ to hit labor $L=1$, and normalize TFP $Z$ to get output $Y=1$." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "@helper\n", - "def steady_state_solution(r, eis, delta, alpha):\n", - " rk = r + delta\n", - " Z = (rk / alpha) ** alpha # normalize so that Y=1\n", - " K = (alpha * Z / rk) ** (1 / (1 - alpha))\n", - " Y = Z * K ** alpha\n", - " w = (1 - alpha) * Z * K ** alpha\n", - " I = delta * K\n", - " C = Y - I\n", - " beta = 1 / (1 + r)\n", - " vphi = w * C ** (-1 / eis)\n", - "\n", - " return Z, K, Y, w, I, C, beta, vphi" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Note 1**: Because the solution is entirely analytical it is not necessary to include any `unknown`s. Also, we can specify `solver=\"solved\"` to avoid using a root-finding algorithm. However, given the present structure of the code, we still require that you provide some target values to verify that the steady state given by the analytical solution indeed produces one that satisfies some set of target equations, like the euler equation, goods market clearing, or Walras' Law, as a gut-check to ensure we don't proceed forward with something we don't want!\n", - "\n", - "**Note 2**: If all of your targets are implicit functions, i.e. you want to target their values equal to 0, you can use a list of their names instead of a dict(ionary)." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "calibration = {\"L\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11}\n", - "blocks = [household, firm, mkt_clearing, steady_state_solution]\n", - "unknowns_ss = {}\n", - "targets_ss = [\"euler\", \"goods_mkt\"]\n", - "ss_helper = sj.steady_state(blocks, calibration, unknowns_ss, targets_ss, solver=\"solved\")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'L': 1.0,\n", - " 'r': 0.009999999999999995,\n", - " 'eis': 1.0,\n", - " 'frisch': 1.0,\n", - " 'delta': 0.025,\n", - " 'alpha': 0.11,\n", - " 'Z': 0.8816460975214567,\n", - " 'K': 3.1428571428571432,\n", - " 'Y': 1.0,\n", - " 'w': 0.8900000000000001,\n", - " 'I': 0.07857142857142874,\n", - " 'C': 0.9214285714285713,\n", - " 'beta': 0.9900990099009901,\n", - " 'vphi': 0.9658914728682173,\n", - " 'goods_mkt': 0.0,\n", - " 'euler': 0.0,\n", - " 'walras': 0.0}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ss_helper" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Gut Check**: To verify that this steady state delivers the same thing that we got from computing it along the standard DAG, we can use one of the developer tools provided in `sequence-jacobian`." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "L resid: 0.0\n", - "Z resid: 5.1535109513167754e-11\n", - "r resid: 9.327198735586961e-12\n", - "eis resid: 0.0\n", - "frisch resid: 0.0\n", - "delta resid: 0.0\n", - "alpha resid: 0.0\n", - "beta resid: 0.0\n", - "vphi resid: 5.6993409991434874e-11\n", - "K resid: 7.346452335355025e-10\n", - "w resid: 2.913902452661432e-11\n", - "Y resid: 3.274069904080079e-11\n", - "C resid: 2.4201751713803787e-11\n", - "I resid: 1.836619745176904e-11\n", - "goods_mkt resid: 7.530864820637362e-11\n", - "euler resid: 1.0022205287896213e-11\n", - "walras resid: 7.530831513946623e-11\n" - ] - } - ], - "source": [ - "import sequence_jacobian.utilities.devtools as dtools\n", - "\n", - "dtools.compare_steady_states(ss, ss_helper)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It checks out!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Steady state in the general case\n", - "\n", - "In practice, your own steady state workflow will likely be somewhere in between the two cases previously described. To compute the steady state it may be easy to analytically solve some portions of the model, but other portions may only be numerically computable, specified as a set of unknowns and targets within a DAG. This may also be desirable for performance reasons, since typically computing a portion of the DAG analytically is less costly than providing the root-finding algorithm with an additional dimension to solve.\n", - "\n", - "Thankfully, any combination of the above two methods is permissible in the `sequence-jacobian` toolkit. You need only provide the analytical solution component specific to the steady state as a `HelperBlock` in the standard list of blocks, specify your unknowns and targets, and call `steady_state`! You don't even need to swap out the `HelperBlock` from the list of blocks when computing general equilibrium Jacobians or computing non-linear transition dynamics." - ] } ], "metadata": { From 263420a93065c672ceacdf8f249a82fb0e8485e5 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 16:01:57 -0600 Subject: [PATCH 006/288] Create new PathTracer class to support debugging --- .../blocks/support/simple_displacement.py | 154 +++++++++++++----- 1 file changed, 114 insertions(+), 40 deletions(-) diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/sequence_jacobian/blocks/support/simple_displacement.py index 8cfd2e0..87342a7 100644 --- a/sequence_jacobian/blocks/support/simple_displacement.py +++ b/sequence_jacobian/blocks/support/simple_displacement.py @@ -37,67 +37,62 @@ def __pos__(self): def __neg__(self): return ignore(-numeric_primitive(self)) - # Tried using the multipledispatch package but @dispatch requires the classes being dispatched on to be defined - # prior to the use of the decorator @dispatch("ClassName"), hence making it impossible to overload in this way, - # as opposed to how isinstance() is evaluated at runtime, so it is valid to check isinstance even if in this module - # the class is defined later on in the module. - # Thus, we need to specially overload the left operations to check if `other` is a Displace to promote properly def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -125,67 +120,62 @@ def __pos__(self): def __neg__(self): return ignore(-numeric_primitive(self)) - # Tried using the multipledispatch package but @dispatch requires the classes being dispatched on to be defined - # prior to the use of the decorator @dispatch("ClassName"), hence making it impossible to overload in this way, - # as opposed to how isinstance() is evaluated at runtime, so it is valid to check isinstance even if in this module - # the class is defined later on in the module. - # Thus, we need to specially overload the left operations to check if `other` is a Displace to promote properly def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -211,61 +201,61 @@ def apply(self, f, **kwargs): return ignore(f(numeric_primitive(self), **kwargs)) def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -704,3 +694,87 @@ def apply_function(func, *args, **kwargs): "Have not yet implemented general apply_function functionality for AccumulatedDerivatives") else: return func(*args, **kwargs) + + +class PathTracer: + """This class behaves like a displacement handler class but exists for the sole purpose of tracing + the path of an input variable through a block to the relevant set of affected output variables""" + + @property + def ss(self): + return self + + def __init__(self, value): + self.value = value + + def __repr__(self): + return f'PathTracer({self.value.__repr__()})' + + def __call__(self, index): + return self + + def apply(self, f, **kwargs): + return PathTracer((f(self.value, **kwargs))) + + def __pos__(self): + return self + + def __neg__(self): + return PathTracer(-self.value) + + def __add__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__add__(other.value)) + else: + return PathTracer(self.value.__add__(other)) + + def __radd__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__radd__(other.value)) + else: + return PathTracer(self.value.__radd__(other)) + + def __sub__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__sub__(other.value)) + else: + return PathTracer(self.value.__sub__(other)) + + def __rsub__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__rsub__(other.value)) + else: + return PathTracer(self.value.__rsub__(other)) + + def __mul__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__mul__(other.value)) + else: + return PathTracer(self.value.__mul__(other)) + + def __rmul__(self, other): + return PathTracer(self.value.__rmul__(other)) + + def __truediv__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__truediv__(other.value)) + else: + return PathTracer(self.value.__truediv__(other)) + + def __rtruediv__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__rtruediv__(other.value)) + else: + return PathTracer(self.value.__rtruediv__(other)) + + def __pow__(self, power, modulo=None): + if isinstance(power, PathTracer): + return PathTracer(self.value.__pow__(power.value)) + else: + return PathTracer(self.value.__pow__(power)) + + def __rpow__(self, other): + if isinstance(other, PathTracer): + return PathTracer(self.value.__rpow__(other.value)) + else: + return PathTracer(self.value.__rpow__(other)) From 0d5336438dfc96e822c6c4562b6755e9923b251f Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 16:04:40 -0600 Subject: [PATCH 007/288] Add __repr__ methods for the other displacement handlers --- .../blocks/support/simple_displacement.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/sequence_jacobian/blocks/support/simple_displacement.py index 87342a7..7fa4473 100644 --- a/sequence_jacobian/blocks/support/simple_displacement.py +++ b/sequence_jacobian/blocks/support/simple_displacement.py @@ -21,6 +21,10 @@ class IgnoreInt(int): Standard arithmetic operators including +, -, x, /, ** all overloaded to "promote" the result of any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore """ + + def __repr__(self): + return f'IgnoreInt({numeric_primitive(self)})' + @property def ss(self): return self @@ -104,6 +108,9 @@ class IgnoreFloat(float): any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore """ + def __repr__(self): + return f'IgnoreFloat({numeric_primitive(self)})' + @property def ss(self): return self @@ -190,6 +197,9 @@ def __new__(cls, x): obj = np.asarray(x).view(cls) return obj + def __repr__(self): + return f'IgnoreVector({numeric_primitive(self)})' + @property def ss(self): return self @@ -271,6 +281,9 @@ def __new__(cls, x, ss=None, name='UNKNOWN'): obj.name = name return obj + def __repr__(self): + return f'Displace({numeric_primitive(self)})' + # TODO: Implemented a very preliminary generalization of Displace to higher-dimensional (>1) ndarrays # however the rigorous operator overloading/testing has not been checked for higher dimensions. # Should also implement some checks for the dimension of .ss, to ensure that it's always N-1 From 4a932296c55f5fdc85efa97ac755732412e91299 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 16:36:13 -0600 Subject: [PATCH 008/288] Remove PathTracer class. Decide to implement `trace_block` a different way that is suitable to all `Block` types --- .../blocks/support/simple_displacement.py | 144 ++++-------------- 1 file changed, 30 insertions(+), 114 deletions(-) diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/sequence_jacobian/blocks/support/simple_displacement.py index 7fa4473..3d63da6 100644 --- a/sequence_jacobian/blocks/support/simple_displacement.py +++ b/sequence_jacobian/blocks/support/simple_displacement.py @@ -42,61 +42,61 @@ def __neg__(self): return ignore(-numeric_primitive(self)) def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -128,61 +128,61 @@ def __neg__(self): return ignore(-numeric_primitive(self)) def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -211,61 +211,61 @@ def apply(self, f, **kwargs): return ignore(f(numeric_primitive(self), **kwargs)) def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__radd__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) + other) def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__add__(numeric_primitive(self)) else: return ignore(other + numeric_primitive(self)) def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rsub__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) - other) def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__sub__(numeric_primitive(self)) else: return ignore(other - numeric_primitive(self)) def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rmul__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) * other) def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__mul__(numeric_primitive(self)) else: return ignore(other * numeric_primitive(self)) def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__rtruediv__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) / other) def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__truediv__(numeric_primitive(self)) else: return ignore(other / numeric_primitive(self)) def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative) or isinstance(power, PathTracer): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): return power.__rpow__(numeric_primitive(self)) else: return ignore(numeric_primitive(self) ** power) def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative) or isinstance(other, PathTracer): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): return other.__pow__(numeric_primitive(self)) else: return ignore(other ** numeric_primitive(self)) @@ -707,87 +707,3 @@ def apply_function(func, *args, **kwargs): "Have not yet implemented general apply_function functionality for AccumulatedDerivatives") else: return func(*args, **kwargs) - - -class PathTracer: - """This class behaves like a displacement handler class but exists for the sole purpose of tracing - the path of an input variable through a block to the relevant set of affected output variables""" - - @property - def ss(self): - return self - - def __init__(self, value): - self.value = value - - def __repr__(self): - return f'PathTracer({self.value.__repr__()})' - - def __call__(self, index): - return self - - def apply(self, f, **kwargs): - return PathTracer((f(self.value, **kwargs))) - - def __pos__(self): - return self - - def __neg__(self): - return PathTracer(-self.value) - - def __add__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__add__(other.value)) - else: - return PathTracer(self.value.__add__(other)) - - def __radd__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__radd__(other.value)) - else: - return PathTracer(self.value.__radd__(other)) - - def __sub__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__sub__(other.value)) - else: - return PathTracer(self.value.__sub__(other)) - - def __rsub__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__rsub__(other.value)) - else: - return PathTracer(self.value.__rsub__(other)) - - def __mul__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__mul__(other.value)) - else: - return PathTracer(self.value.__mul__(other)) - - def __rmul__(self, other): - return PathTracer(self.value.__rmul__(other)) - - def __truediv__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__truediv__(other.value)) - else: - return PathTracer(self.value.__truediv__(other)) - - def __rtruediv__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__rtruediv__(other.value)) - else: - return PathTracer(self.value.__rtruediv__(other)) - - def __pow__(self, power, modulo=None): - if isinstance(power, PathTracer): - return PathTracer(self.value.__pow__(power.value)) - else: - return PathTracer(self.value.__pow__(power)) - - def __rpow__(self, other): - if isinstance(other, PathTracer): - return PathTracer(self.value.__rpow__(other.value)) - else: - return PathTracer(self.value.__rpow__(other)) From d0070450972f09f5455dd8a844a48765c8393c00 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Feb 2021 18:06:46 -0600 Subject: [PATCH 009/288] Create new jacobian sub-package in major overhaul Additional changes: - Now all blocks return a JacobianDict when the .jac method is invoked --- sequence_jacobian/__init__.py | 2 +- sequence_jacobian/asymptotic.py | 6 +- sequence_jacobian/blocks/het_block.py | 5 +- sequence_jacobian/blocks/simple_block.py | 22 +- sequence_jacobian/blocks/solved_block.py | 10 +- sequence_jacobian/jacobian/__init__.py | 1 + .../classes.py} | 234 ++++++++++----- .../{jacobian.py => jacobian/drivers.py} | 281 ++---------------- sequence_jacobian/jacobian/support.py | 139 +++++++++ sequence_jacobian/nonlinear.py | 11 +- sequence_jacobian/utilities/__init__.py | 2 +- tests/base/test_determinacy.py | 10 +- tests/base/test_estimation.py | 11 +- tests/base/test_jacobian.py | 35 ++- tests/base/test_simple_block.py | 3 + tests/base/test_steady_state.py | 1 - tests/base/test_transitional_dynamics.py | 25 +- 17 files changed, 402 insertions(+), 396 deletions(-) create mode 100644 sequence_jacobian/jacobian/__init__.py rename sequence_jacobian/{utilities/special_matrices.py => jacobian/classes.py} (59%) rename sequence_jacobian/{jacobian.py => jacobian/drivers.py} (61%) create mode 100644 sequence_jacobian/jacobian/support.py diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index dfaea3d..68eca9b 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -14,7 +14,7 @@ from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved from .steady_state import steady_state -from .jacobian import get_G, get_H_U, get_impulse +from .jacobian.drivers import get_G, get_H_U, get_impulse from .nonlinear import td_solve from .utilities import discretize from .utilities import interpolate diff --git a/sequence_jacobian/asymptotic.py b/sequence_jacobian/asymptotic.py index a42a8df..906add9 100644 --- a/sequence_jacobian/asymptotic.py +++ b/sequence_jacobian/asymptotic.py @@ -3,8 +3,8 @@ import numpy as np from numpy.fft import rfft, rfftn, irfft, irfftn -from . import jacobian as jac from . import determinacy +from .jacobian.support import pack_asymptotic_jacobians, unpack_asymptotic_jacobians class AsymptoticTimeInvariant: @@ -179,7 +179,7 @@ def invert_jacdict(jacdict, unknowns, targets, tau, test_invertible=False): assert k == len(targets) # stack the k^2 Jacobians relating unknowns to targets into an A matrix - A = jac.pack_asymptotic_jacobians(jacdict, unknowns, targets, tau) + A = pack_asymptotic_jacobians(jacdict, unknowns, targets, tau) if test_invertible: # use winding number criterion to test invertibility @@ -205,4 +205,4 @@ def invert_jacdict(jacdict, unknowns, targets, tau, test_invertible=False): A_inv = irfftn(A_rfft_inv, s=(4*tau-3,), axes=(0,))[:2*tau-1, :, :] # unstack this - return jac.unpack_asymptotic_jacobians(A_inv, targets, unknowns, tau) + return unpack_asymptotic_jacobians(A_inv, targets, unknowns, tau) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index ff99cc5..94cb48a 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -3,6 +3,7 @@ from .. import utilities as utils from .. import asymptotic +from ..jacobian.classes import JacobianDict def het(exogenous, policy, backward, backward_init=None): @@ -408,9 +409,9 @@ def jac(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved if save: self.saved_shock_list, self.saved_output_list = relevant_shocks, output_list - self.saved = {'curlyYs' : curlyYs, 'curlyDs' : curlyDs, 'curlyPs' : curlyPs, 'F': F, 'J': J} + self.saved = {'curlyYs': curlyYs, 'curlyDs': curlyDs, 'curlyPs': curlyPs, 'F': F, 'J': J} - return J + return JacobianDict(J) def ajac(self, ss, T, shock_list, output_list=None, h=1E-4, Tpost=None, save=False, use_saved=False): """Like .jac, but outputs asymptotic columns of Jacobians as AsymptoticTimeInvariant objects diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 2fe92ba..72a3d1b 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -1,8 +1,8 @@ import numpy as np -from .. import utilities as utils -from .. import jacobian from .support.simple_displacement import ignore, numeric_primitive, Displace, AccumulatedDerivative +from ..jacobian.classes import JacobianDict, SimpleSparse +from ..utilities import misc '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -28,8 +28,8 @@ def production(Z, K, L, alpha): def __init__(self, f): self.f = f - self.input_list = utils.misc.input_list(f) - self.output_list = utils.misc.output_list(f) + self.input_list = misc.input_list(f) + self.output_list = misc.output_list(f) self.inputs = set(self.input_list) self.outputs = set(self.output_list) @@ -67,9 +67,9 @@ def _output_in_td_format(self, **kwargs_new): # Because we know at least one of the outputs in `out` must be of length T T = np.max([np.size(o) for o in out]) out_unif_dim = [np.full(T, numeric_primitive(o)) if np.isscalar(o) else numeric_primitive(o) for o in out] - return dict(zip(self.output_list, utils.misc.make_tuple(out_unif_dim))) + return dict(zip(self.output_list, misc.make_tuple(out_unif_dim))) else: - return dict(zip(self.output_list, utils.misc.make_tuple(numeric_primitive(out)))) + return dict(zip(self.output_list, misc.make_tuple(numeric_primitive(out)))) def td(self, ss, **kwargs): kwargs_new = {} @@ -140,17 +140,17 @@ def jac(self, ss, T=None, shock_list=[]): if not J[o]: del J[o] - return J + return JacobianDict(J) def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" - input_args = {i: ignore(steady_state_dict[i]) for i in utils.misc.input_list(f)} + input_args = {i: ignore(steady_state_dict[i]) for i in misc.input_list(f)} input_args[shock_name] = AccumulatedDerivative(f_value=steady_state_dict[shock_name]) - J = {o: {} for o in utils.misc.output_list(f)} - for o, o_name in zip(utils.misc.make_tuple(f(**input_args)), utils.misc.output_list(f)): + J = {o: {} for o in misc.output_list(f)} + for o, o_name in zip(misc.make_tuple(f(**input_args)), misc.output_list(f)): if isinstance(o, AccumulatedDerivative): - J[o_name] = utils.special_matrices.SimpleSparse(o.elements) + J[o_name] = SimpleSparse(o.elements) return J diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 09a0c49..5f422d7 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,5 +1,5 @@ from .. import nonlinear -from .. import jacobian as jac +from ..jacobian.drivers import get_G, get_G_asymptotic from ..steady_state import steady_state from ..blocks.simple_block import simple @@ -64,13 +64,13 @@ def jac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False): relevant_shocks = [i for i in self.inputs if i in shock_list] # H_U_factored caching could be helpful here too - return jac.get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, - T, ss, output_list, save=save, use_saved=use_saved) + return get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, + T, ss, output_list, save=save, use_saved=use_saved) def ajac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False, Tpost=None): relevant_shocks = [i for i in self.inputs if i in shock_list] if Tpost is None: Tpost = 2*T - return jac.get_G_asymptotic(self.block_list, relevant_shocks, list(self.unknowns.keys()), - self.targets, T, ss, output_list, save=save, use_saved=use_saved, Tpost=Tpost) + return get_G_asymptotic(self.block_list, relevant_shocks, list(self.unknowns.keys()), + self.targets, T, ss, output_list, save=save, use_saved=use_saved, Tpost=Tpost) diff --git a/sequence_jacobian/jacobian/__init__.py b/sequence_jacobian/jacobian/__init__.py new file mode 100644 index 0000000..57070a8 --- /dev/null +++ b/sequence_jacobian/jacobian/__init__.py @@ -0,0 +1 @@ +"""Jacobian computation and support functions""" diff --git a/sequence_jacobian/utilities/special_matrices.py b/sequence_jacobian/jacobian/classes.py similarity index 59% rename from sequence_jacobian/utilities/special_matrices.py rename to sequence_jacobian/jacobian/classes.py index c9f6716..a1bb843 100644 --- a/sequence_jacobian/utilities/special_matrices.py +++ b/sequence_jacobian/jacobian/classes.py @@ -1,9 +1,10 @@ -"""Matrices with special structure, which work with simple, efficient rules""" +"""Various classes to support the computation of Jacobians""" -from .. import asymptotic -import numpy as np -from numba import njit import copy +import numpy as np + +from . import support +from .. import asymptotic class IdentityMatrix: @@ -196,14 +197,14 @@ def __neg__(self): def __matmul__(self, A): if isinstance(A, SimpleSparse): # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs - return multiply_rs_rs(self, A) + return SimpleSparse(support.multiply_rs_rs(self, A)) elif isinstance(A, np.ndarray): # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing indices, xs = self.array() if A.ndim == 2: - return multiply_rs_matrix(indices, xs, A) + return support.multiply_rs_matrix(indices, xs, A) elif A.ndim == 1: - return multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] + return support.multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] else: return NotImplemented else: @@ -274,74 +275,161 @@ def __eq__(self, s): return self.elements == s.elements -def multiply_basis(t1, t2): - """Matrix multiplication operation mapping two sparse basis elements to another.""" - # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with - # signs of i and j flipped to reflect different sign convention used here - i, m = t1 - j, n = t2 - k = i + j - if i >= 0: - if j >= 0: - l = max(m, n - i) - elif k >= 0: - l = max(m, n - k) - else: - l = max(m + k, n) - else: - if j <= 0: - l = max(m + j, n) +class NestedDict: + def __init__(self, nesteddict, outputs=None, inputs=None): + if isinstance(nesteddict, NestedDict): + self.nesteddict = nesteddict.nesteddict + self.outputs = nesteddict.outputs + self.inputs = nesteddict.inputs else: - l = max(m, n) + min(-i, j) - return k, l - - -def multiply_rs_rs(s1, s2): - """Matrix multiplication operation on two SimpleSparse objects.""" - # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, - # add all pairwise products to get overall product - elements = {} - for im, x in s1.elements.items(): - for jn, y in s2.elements.items(): - kl = multiply_basis(im, jn) - if kl in elements: - elements[kl] += x * y + self.nesteddict = nesteddict + if outputs is None: + outputs = list(nesteddict.keys()) + if inputs is None: + inputs = [] + for v in nesteddict.values(): + inputs.extend(list(v)) + inputs = deduplicate(inputs) + + self.outputs = list(outputs) + self.inputs = list(inputs) + + def __repr__(self): + return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' + + def __iter__(self): + return iter(self.outputs) + + def __or__(self, other): + # non-in-place merge: make a copy, then update + merged = type(self)(self.nesteddict, self.outputs, self.inputs) + merged.update(other) + return merged + + def __getitem__(self, x): + if isinstance(x, str): + # case 1: just a single output, give subdict + return self.nesteddict[x] + elif isinstance(x, tuple): + # case 2: tuple, referring to output and input + o, i = x + o = self.outputs if o == slice(None, None, None) else o + i = self.inputs if i == slice(None, None, None) else i + if isinstance(o, str): + if isinstance(i, str): + # case 2a: one output, one input, return single Jacobian + return self.nesteddict[o][i] + else: + # case 2b: one output, multiple inputs, return dict + return {ii: self.nesteddict[o][ii] for ii in i} else: - elements[kl] = x * y - return SimpleSparse(elements) - - -@njit -def multiply_rs_matrix(indices, xs, A): - """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. - Much more computationally demanding than multiplying two SimpleSparse (which is almost - free with simple analytical formula), so we implement as jitted function.""" - n = indices.shape[0] - T = A.shape[0] - S = A.shape[1] - Aout = np.zeros((T, S)) - - for count in range(n): - # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' - # was stored in 'indices' and 'xs' - i = indices[count, 0] - m = indices[count, 1] - x = xs[count] - - # loop faster than vectorized when jitted - # directly use def of basis element (i, m), displacement of i and ignore first m - if i == 0: - for t in range(m, T): - for s in range(S): - Aout[t, s] += x * A[t, s] - elif i > 0: - for t in range(m, T - i): - for s in range(S): - Aout[t, s] += x * A[t + i, s] + # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i + i = (i,) if isinstance(i, str) else i + return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) + elif isinstance(x, list) or isinstance(x, set): + # case 3: assume that list or set refers just to outputs, get all of those + return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) else: - for t in range(m - i, T): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - return Aout + raise ValueError(f'Tried to get impermissible item {x}') + + def get(self, *args, **kwargs): + # this is for compatibilty, not a huge fan + return self.nesteddict.get(*args, **kwargs) + + def update(self, J): + if set(self.inputs) != set(J.inputs): + raise ValueError \ + (f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') + if not set(self.outputs).isdisjoint(J.outputs): + raise ValueError \ + (f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') + self.outputs = self.outputs + J.outputs + self.nesteddict = {**self.nesteddict, **J.nesteddict} + + def complete(self, filler): + nesteddict = {} + for o in self.outputs: + nesteddict[o] = dict(self.nesteddict[o]) + for i in self.inputs: + if i not in nesteddict[o]: + nesteddict[o][i] = filler + return type(self)(nesteddict, self.outputs, self.inputs) + + +def deduplicate(mylist): + """Remove duplicates while otherwise maintaining order""" + return list(dict.fromkeys(mylist)) + + +class JacobianDict(NestedDict): + @staticmethod + def identity(ks): + return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() + + def complete(self): + return super().complete(ZeroMatrix()) + + def addinputs(self): + """Add any inputs that were not already in output list as outputs, with the identity""" + inputs = [x for x in self.inputs if x not in self.outputs] + return self | JacobianDict.identity(inputs) + def __matmul__(self, x): + if isinstance(x, JacobianDict): + return self.compose(x) + else: + return self.apply(x) + + def compose(self, J): + o_list = self.outputs + m_list = tuple(set(self.inputs) & set(J.outputs)) + i_list = J.inputs + + J_om = self.nesteddict + J_mi = J.nesteddict + J_oi = {} + + for o in o_list: + J_oi[o] = {} + for i in i_list: + Jout = ZeroMatrix() + for m in m_list: + J_om[o][m] + J_mi[m][i] + Jout += J_om[o][m] @ J_mi[m][i] + J_oi[o][i] = Jout + + return JacobianDict(J_oi, o_list, i_list) + + def apply(self, x): + # assume that all entries in x have some length T, and infer it + T = len(next(iter(x.values()))) + + inputs = x.keys() & set(self.inputs) + J_oi = self.nesteddict + y = {} + + for o in self.outputs: + y[o] = np.zeros(T) + for i in inputs: + y[o] += J_oi[o][i] @ x[i] + + return y + + def pack(self, T): + J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) + for iO, O in enumerate(self.outputs): + for iI, I in enumerate(self.inputs): + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = support.make_matrix(self[O, I], T) + return J + + @staticmethod + def unpack(bigjac, outputs, inputs, T): + """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" + jacdict = {} + for iO, O in enumerate(outputs): + jacdict[O] = {} + for iI, I in enumerate(inputs): + jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] + return JacobianDict(jacdict, outputs, inputs) diff --git a/sequence_jacobian/jacobian.py b/sequence_jacobian/jacobian/drivers.py similarity index 61% rename from sequence_jacobian/jacobian.py rename to sequence_jacobian/jacobian/drivers.py index cde8487..7c631a4 100644 --- a/sequence_jacobian/jacobian.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -1,14 +1,13 @@ -"""Methods for computing and manipulating both block-level and model-level Jacobians""" +"""Main methods (drivers) for computing and manipulating both block-level and model-level Jacobians""" import numpy as np -import copy -from numba import njit -from . import utilities as utils -from . import asymptotic -from .utilities.special_matrices import IdentityMatrix, ZeroMatrix +from .classes import JacobianDict +from .support import pack_vectors, unpack_vectors, pack_asymptotic_jacobians +from .. import asymptotic +from ..utilities import misc, graph -'''Part 1: High-level convenience routines: +'''Drivers: - get_H_U : get H_U matrix mapping all unknowns to all targets - get_impulse : get single GE impulse response - get_G : get G matrices characterizing all GE impulse responses @@ -37,7 +36,7 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=N Returns ------- - H_U : + H_U : if asymptotic=False: array(T*n_u*T*n_u) H_U, Jacobian mapping all unknowns to all targets is asymptotic=True: @@ -53,11 +52,11 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=N if not asymptotic: # pack these n_u^2 matrices, each T*T, into a single matrix return H_U_unpacked[targets, unknowns].pack(T) - #return pack_jacobians(H_U_unpacked, unknowns, targets, T) + # return pack_jacobians(H_U_unpacked, unknowns, targets, T) else: # pack these n_u^2 AsymptoticTimeInvariant objects into a single (2*Tpost-1,n_u,n_u) array if Tpost is None: - Tpost = 2*T + Tpost = 2 * T return pack_asymptotic_jacobians(H_U_unpacked, unknowns, targets, Tpost) @@ -110,14 +109,14 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None # step 3: solve H_UdU = -H_ZdZ for dU if H_U is None and H_U_factored is None: H_U = H_U_unpacked[targets, unknowns].pack(T) - #H_U = pack_jacobians(H_U_unpacked, unknowns, targets, T) - + # H_U = pack_jacobians(H_U_unpacked, unknowns, targets, T) + H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) if H_U_factored is None: - dU_packed = - np.linalg.solve(H_U, H_ZdZ_packed) + dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) else: - dU_packed = - utils.misc.factored_solve(H_U_factored, H_ZdZ_packed) + dU_packed = -misc.factored_solve(H_U_factored, H_ZdZ_packed) dU = unpack_vectors(dU_packed, unknowns, T) @@ -129,7 +128,7 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None return {o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs} -def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, +def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, H_U=None, H_U_factored=None, save=False, use_saved=False): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -171,14 +170,14 @@ def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, # step 3: solve for G^U, unpack if H_U is None and H_U_factored is None: H_U = J_curlyH_U[targets, unknowns].pack(T) - #H_U = pack_jacobians(J_curlyH_U, unknowns, targets, T) + # H_U = pack_jacobians(J_curlyH_U, unknowns, targets, T) H_Z = J_curlyH_Z[targets, exogenous].pack(T) - #H_Z = pack_jacobians(J_curlyH_Z, exogenous, targets, T) + # H_Z = pack_jacobians(J_curlyH_Z, exogenous, targets, T) if H_U_factored is None: G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) else: - G_U = JacobianDict.unpack(-utils.misc.factored_solve(H_U_factored, H_Z), unknowns, exogenous, T) + G_U = JacobianDict.unpack(-misc.factored_solve(H_U_factored, H_Z), unknowns, exogenous, T) # step 4: forward accumulation to get all outputs starting with G_U # by default, don't calculate targets! @@ -188,18 +187,18 @@ def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def get_G_asymptotic(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, +def get_G_asymptotic(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, save=False, use_saved=False, Tpost=None): """Like get_G, but rather than returning the actual matrices G, return asymptotic.AsymptoticTimeInvariant objects representing their asymptotic columns.""" # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(block_list, unknowns + exogenous, ss, T, save=save, + curlyJs, required = curlyJ_sorted(block_list, unknowns + exogenous, ss, T, save=save, use_saved=use_saved, asymptotic=True, Tpost=Tpost) # step 2: do (matrix) forward accumulation to get # H_U = J^(curlyH, curlyU) - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) + J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) # step 3: invert H_U and forward accumulate to get G_U = H_U^(-1)H_Z U_H_unpacked = asymptotic.invert_jacdict(J_curlyH_U, unknowns, targets, Tpost) @@ -210,7 +209,7 @@ def get_G_asymptotic(block_list, exogenous, unknowns, targets, T, ss=None, outpu curlyJs = [G_U] + curlyJs if outputs is None: outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) + return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=None, save=False, use_saved=False): @@ -236,8 +235,8 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=N """ # step 1: get topological sort and required - topsorted = utils.graph.block_sort(block_list, ignore_helpers=True) - required = utils.graph.find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=True) + topsorted = graph.block_sort(block_list, ignore_helpers=True) + required = graph.find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=True) # Remove any vector-valued outputs that are intermediate inputs, since we don't want # to compute Jacobians with respect to vector-valued variables @@ -314,7 +313,7 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): if jacflag: # Jacobians of inputs with respect to themselves are the identity, initialize with this - #out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} + # out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} out = JacobianDict.identity(inputs) else: out = inputs.copy() @@ -324,19 +323,19 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): curlyJ = JacobianDict(curlyJ).complete() if alloutputs is not None: # if we want specific list of outputs, restrict curlyJ to that before continuing - #curlyJ = {k: v for k, v in curlyJ.items() if k in alloutputs} + # curlyJ = {k: v for k, v in curlyJ.items() if k in alloutputs} curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] if jacflag: - #out.update(compose_jacobians(out, curlyJ)) + # out.update(compose_jacobians(out, curlyJ)) out.update(curlyJ.compose(out)) else: - #out.update(apply_jacobians(curlyJ, out)) + # out.update(apply_jacobians(curlyJ, out)) out.update(curlyJ.apply(out)) if outputs is not None: # if we want specific list of outputs, restrict to that # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) - #return {k: out[k] for k in outputs if k in out} + # return {k: out[k] for k in outputs if k in out} return out[[k for k in outputs if k in out.outputs]] else: return out @@ -347,227 +346,3 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): # else: # # default behavior for case where we're calculating paths: return everything, including inputs # return out - - -'''Part 2: Somewhat lower-level routines for handling Jacobians''' - - -def pack_asymptotic_jacobians(jacdict, inputs, outputs, tau): - """If we have -(tau-1),...,(tau-1) AsymptoticTimeInvariant Jacobians (or SimpleSparse) from - nI inputs to nO outputs in jacdict, combine into (2*tau-1,nO,nI) array A""" - nI, nO = len(inputs), len(outputs) - A = np.empty((2*tau-1, nI, nO)) - for iO in range(nO): - subdict = jacdict.get(outputs[iO], {}) - for iI in range(nI): - if inputs[iI] in subdict: - A[:, iO, iI] = make_ATI_v(jacdict[outputs[iO]][inputs[iI]], tau) - else: - A[:, iO, iI] = 0 - return A - - -def unpack_asymptotic_jacobians(A, inputs, outputs, tau): - """If we have (2*tau-1, nO, nI) array A where each A[:,o,i] is vector for AsymptoticTimeInvariant - Jacobian mapping output o to output i, output nested dict of AsymptoticTimeInvariant objects""" - nI, nO = len(inputs), len(outputs) - - jacdict = {} - for iO in range(nO): - jacdict[outputs[iO]] = {} - for iI in range(nI): - jacdict[outputs[iO]][inputs[iI]] = asymptotic.AsymptoticTimeInvariant(A[:, iO, iI]) - return jacdict - - -def pack_vectors(vs, names, T): - v = np.zeros(len(names)*T) - for i, name in enumerate(names): - if name in vs: - v[i*T:(i+1)*T] = vs[name] - return v - - -def unpack_vectors(v, names, T): - vs = {} - for i, name in enumerate(names): - vs[name] = v[i*T:(i+1)*T] - return vs - - -def make_matrix(A, T): - """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method - to convert it to T*T array.""" - if not isinstance(A, np.ndarray): - return A.matrix(T) - else: - return A - - -def make_ATI_v(x, tau): - """If x is either a AsymptoticTimeInvariant or something that can be converted to it, e.g. - SimpleSparse, report the underlying length 2*tau-1 vector with entries -(tau-1),...,(tau-1)""" - if not isinstance(x, asymptotic.AsymptoticTimeInvariant): - return x.asymptotic_time_invariant.changetau(tau).v - else: - return x.v - - -"""Experimental new jacdict feature""" - -class NestedDict: - def __init__(self, nesteddict, outputs=None, inputs=None): - if isinstance(nesteddict, NestedDict): - self.nesteddict = nesteddict.nesteddict - self.outputs = nesteddict.outputs - self.inputs = nesteddict.inputs - else: - self.nesteddict = nesteddict - if outputs is None: - outputs = list(nesteddict.keys()) - if inputs is None: - inputs = [] - for v in nesteddict.values(): - inputs.extend(list(v)) - inputs = deduplicate(inputs) - - self.outputs = list(outputs) - self.inputs = list(inputs) - - def __repr__(self): - return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' - - def __iter__(self): - return iter(self.outputs) - - def __or__(self, other): - # non-in-place merge: make a copy, then update - merged = type(self)(self.nesteddict, self.outputs, self.inputs) - merged.update(other) - return merged - - def __getitem__(self, x): - if isinstance(x, str): - # case 1: just a single output, give subdict - return self.nesteddict[x] - elif isinstance(x, tuple): - # case 2: tuple, referring to output and input - o, i = x - o = self.outputs if o == slice(None, None, None) else o - i = self.inputs if i == slice(None, None, None) else i - if isinstance(o, str): - if isinstance(i, str): - # case 2a: one output, one input, return single Jacobian - return self.nesteddict[o][i] - else: - # case 2b: one output, multiple inputs, return dict - return {ii: self.nesteddict[o][ii] for ii in i} - else: - # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i - i = (i,) if isinstance(i, str) else i - return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) - elif isinstance(x, list) or isinstance(x, set): - # case 3: assume that list or set refers just to outputs, get all of those - return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) - else: - raise ValueError(f'Tried to get impermissible item {x}') - - def get(self, *args, **kwargs): - # this is for compatibilty, not a huge fan - return self.nesteddict.get(*args, **kwargs) - - def update(self, J): - if set(self.inputs) != set(J.inputs): - raise ValueError(f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') - if not set(self.outputs).isdisjoint(J.outputs): - raise ValueError(f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') - self.outputs = self.outputs + J.outputs - self.nesteddict = {**self.nesteddict, **J.nesteddict} - - def complete(self, filler): - nesteddict = {} - for o in self.outputs: - nesteddict[o] = dict(self.nesteddict[o]) - for i in self.inputs: - if i not in nesteddict[o]: - nesteddict[o][i] = filler - return type(self)(nesteddict, self.outputs, self.inputs) - - -def deduplicate(mylist): - """Remove duplicates while otherwise maintaining order""" - return list(dict.fromkeys(mylist)) - - -class JacobianDict(NestedDict): - @staticmethod - def identity(ks): - return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() - - def complete(self): - return super().complete(ZeroMatrix()) - - def addinputs(self): - """Add any inputs that were not already in output list as outputs, with the identity""" - inputs = [x for x in self.inputs if x not in self.outputs] - return self | JacobianDict.identity(inputs) - - def __matmul__(self, x): - if isinstance(x, JacobianDict): - return self.compose(x) - else: - return self.apply(x) - - def compose(self, J): - o_list = self.outputs - m_list = tuple(set(self.inputs) & set(J.outputs)) - i_list = J.inputs - - J_om = self.nesteddict - J_mi = J.nesteddict - J_oi = {} - - for o in o_list: - J_oi[o] = {} - for i in i_list: - Jout = ZeroMatrix() - for m in m_list: - J_om[o][m] - J_mi[m][i] - Jout += J_om[o][m] @ J_mi[m][i] - J_oi[o][i] = Jout - - return JacobianDict(J_oi, o_list, i_list) - - def apply(self, x): - # assume that all entries in x have some length T, and infer it - T = len(next(iter(x.values()))) - - inputs = x.keys() & set(self.inputs) - J_oi = self.nesteddict - y = {} - - for o in self.outputs: - y[o] = np.zeros(T) - for i in inputs: - y[o] += J_oi[o][i] @ x[i] - - return y - - def pack(self, T): - J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) - for iO, O in enumerate(self.outputs): - for iI, I in enumerate(self.inputs): - J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(self[O, I], T) - return J - - @staticmethod - def unpack(bigjac, outputs, inputs, T): - """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" - jacdict = {} - for iO, O in enumerate(outputs): - jacdict[O] = {} - for iI, I in enumerate(inputs): - jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] - return JacobianDict(jacdict, outputs, inputs) - diff --git a/sequence_jacobian/jacobian/support.py b/sequence_jacobian/jacobian/support.py new file mode 100644 index 0000000..d908a31 --- /dev/null +++ b/sequence_jacobian/jacobian/support.py @@ -0,0 +1,139 @@ +"""Various lower-level functions to support the computation of Jacobians""" + +import numpy as np +from numba import njit + +from .. import asymptotic + + +# For supporting SimpleSparse +def multiply_basis(t1, t2): + """Matrix multiplication operation mapping two sparse basis elements to another.""" + # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with + # signs of i and j flipped to reflect different sign convention used here + i, m = t1 + j, n = t2 + k = i + j + if i >= 0: + if j >= 0: + l = max(m, n - i) + elif k >= 0: + l = max(m, n - k) + else: + l = max(m + k, n) + else: + if j <= 0: + l = max(m + j, n) + else: + l = max(m, n) + min(-i, j) + return k, l + + +def multiply_rs_rs(s1, s2): + """Matrix multiplication operation on two SimpleSparse objects.""" + # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, + # add all pairwise products to get overall product + elements = {} + for im, x in s1.elements.items(): + for jn, y in s2.elements.items(): + kl = multiply_basis(im, jn) + if kl in elements: + elements[kl] += x * y + else: + elements[kl] = x * y + return elements + + +@njit +def multiply_rs_matrix(indices, xs, A): + """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. + Much more computationally demanding than multiplying two SimpleSparse (which is almost + free with simple analytical formula), so we implement as jitted function.""" + n = indices.shape[0] + T = A.shape[0] + S = A.shape[1] + Aout = np.zeros((T, S)) + + for count in range(n): + # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' + # was stored in 'indices' and 'xs' + i = indices[count, 0] + m = indices[count, 1] + x = xs[count] + + # loop faster than vectorized when jitted + # directly use def of basis element (i, m), displacement of i and ignore first m + if i == 0: + for t in range(m, T): + for s in range(S): + Aout[t, s] += x * A[t, s] + elif i > 0: + for t in range(m, T - i): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + else: + for t in range(m - i, T): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + return Aout + + +def pack_asymptotic_jacobians(jacdict, inputs, outputs, tau): + """If we have -(tau-1),...,(tau-1) AsymptoticTimeInvariant Jacobians (or SimpleSparse) from + nI inputs to nO outputs in jacdict, combine into (2*tau-1,nO,nI) array A""" + nI, nO = len(inputs), len(outputs) + A = np.empty((2*tau-1, nI, nO)) + for iO in range(nO): + subdict = jacdict.get(outputs[iO], {}) + for iI in range(nI): + if inputs[iI] in subdict: + A[:, iO, iI] = make_ATI_v(jacdict[outputs[iO]][inputs[iI]], tau) + else: + A[:, iO, iI] = 0 + return A + + +def unpack_asymptotic_jacobians(A, inputs, outputs, tau): + """If we have (2*tau-1, nO, nI) array A where each A[:,o,i] is vector for AsymptoticTimeInvariant + Jacobian mapping output o to output i, output nested dict of AsymptoticTimeInvariant objects""" + nI, nO = len(inputs), len(outputs) + + jacdict = {} + for iO in range(nO): + jacdict[outputs[iO]] = {} + for iI in range(nI): + jacdict[outputs[iO]][inputs[iI]] = asymptotic.AsymptoticTimeInvariant(A[:, iO, iI]) + return jacdict + + +def pack_vectors(vs, names, T): + v = np.zeros(len(names)*T) + for i, name in enumerate(names): + if name in vs: + v[i*T:(i+1)*T] = vs[name] + return v + + +def unpack_vectors(v, names, T): + vs = {} + for i, name in enumerate(names): + vs[name] = v[i*T:(i+1)*T] + return vs + + +def make_matrix(A, T): + """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method + to convert it to T*T array.""" + if not isinstance(A, np.ndarray): + return A.matrix(T) + else: + return A + + +def make_ATI_v(x, tau): + """If x is either a AsymptoticTimeInvariant or something that can be converted to it, e.g. + SimpleSparse, report the underlying length 2*tau-1 vector with entries -(tau-1),...,(tau-1)""" + if not isinstance(x, asymptotic.AsymptoticTimeInvariant): + return x.asymptotic_time_invariant.changetau(tau).v + else: + return x.v diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index d942b68..e28bc94 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -3,8 +3,9 @@ import numpy as np from . import utilities as utils -from . import jacobian as jac from .blocks import het_block as het +from .jacobian.drivers import get_H_U +from .jacobian.support import pack_vectors, unpack_vectors def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, monotonic=False, @@ -48,13 +49,13 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon # initialize guess for unknowns to steady state length T Us = {k: np.full(T, ss[k]) for k in unknowns} - Uvec = jac.pack_vectors(Us, unknowns, T) + Uvec = pack_vectors(Us, unknowns, T) # obtain H_U_factored if we don't have it already if H_U_factored is None: if H_U is None: # not even H_U is supplied, get it (costly if there are HetBlocks) - H_U = jac.get_H_U(block_list, unknowns, targets, T, ss, save=save, use_saved=use_saved) + H_U = get_H_U(block_list, unknowns, targets, T, ss, save=save, use_saved=use_saved) H_U_factored = utils.misc.factor(H_U) # do a topological sort once to avoid some redundancy @@ -72,9 +73,9 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon break else: # update guess U by -H_U^(-1) times errors H - Hvec = jac.pack_vectors(results, targets, T) + Hvec = pack_vectors(results, targets, T) Uvec -= utils.misc.factored_solve(H_U_factored, Hvec) - Us = jac.unpack_vectors(Uvec, unknowns, T) + Us = unpack_vectors(Uvec, unknowns, T) else: raise ValueError(f'No convergence after {maxit} backward iterations!') diff --git a/sequence_jacobian/utilities/__init__.py b/sequence_jacobian/utilities/__init__.py index ba3a615..e8d1bab 100644 --- a/sequence_jacobian/utilities/__init__.py +++ b/sequence_jacobian/utilities/__init__.py @@ -1,3 +1,3 @@ """Utilities relating to: interpolation, forward step/transition, grids and Markov chains, solvers, sorting, etc.""" -from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers, special_matrices +from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers diff --git a/tests/base/test_determinacy.py b/tests/base/test_determinacy.py index e1b9b2e..dd85415 100644 --- a/tests/base/test_determinacy.py +++ b/tests/base/test_determinacy.py @@ -1,7 +1,5 @@ """Test all models' determinacy calculations""" -import pytest - -from sequence_jacobian import jacobian, determinacy +from sequence_jacobian import determinacy, get_H_U def test_hank_determinacy(one_asset_hank_model): @@ -9,13 +7,13 @@ def test_hank_determinacy(one_asset_hank_model): T = 100 # Stable Case - A = jacobian.get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True, save=True) + A = get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True, save=True) wn = determinacy.winding_criterion(A) assert wn == 0 # Unstable Case ss_unstable = {**ss, "phi": 0.75} - A_unstable = jacobian.get_H_U(blocks, unknowns, targets, T, ss_unstable, asymptotic=True, use_saved=True) + A_unstable = get_H_U(blocks, unknowns, targets, T, ss_unstable, asymptotic=True, use_saved=True) wn_unstable = determinacy.winding_criterion(A_unstable) assert wn_unstable == -1 @@ -24,6 +22,6 @@ def test_two_asset_determinacy(two_asset_hank_model): blocks, exogenous, unknowns, targets, ss = two_asset_hank_model T = 100 - A = jacobian.get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True) + A = get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True) wn = determinacy.winding_criterion(A) assert wn == 0 \ No newline at end of file diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 58df805..14e0bd1 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -3,8 +3,7 @@ import pytest import numpy as np -import sequence_jacobian as sj -from sequence_jacobian import jacobian +from sequence_jacobian import get_G, estimation # See test_determinacy.py for the to-do describing this suppression @@ -14,8 +13,8 @@ def test_krusell_smith_estimation(krusell_smith_model): np.random.seed(41234) T = 50 - G = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss) # Step 1: Stacked impulse responses rho = 0.9 @@ -34,7 +33,7 @@ def test_krusell_smith_estimation(krusell_smith_model): # Step 2: Obtain covariance at all leads and lags sigmas = np.array([sigma_persist, sigma_trans]) - Sigma = sj.estimation.all_covariances(dX, sigmas) + Sigma = estimation.all_covariances(dX, sigmas) # Step 3: Log-likelihood calculation # random 100 observations @@ -44,5 +43,5 @@ def test_krusell_smith_estimation(krusell_smith_model): sigma_measurement = np.full(4, 0.05) # calculate log-likelihood - ll = sj.estimation.log_likelihood(Y, Sigma, sigma_measurement) + ll = estimation.log_likelihood(Y, Sigma, sigma_measurement) assert np.isclose(ll, -59921.410111251025) \ No newline at end of file diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index f477aa5..dc5af0b 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -2,7 +2,8 @@ import numpy as np -from sequence_jacobian import jacobian +from sequence_jacobian.jacobian.drivers import get_G, forward_accumulate, curlyJ_sorted +from sequence_jacobian.jacobian.classes import JacobianDict def test_ks_jac(krusell_smith_model): @@ -11,21 +12,23 @@ def test_ks_jac(krusell_smith_model): T = 10 # Automatically calculate the general equilibrium Jacobian - G2 = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G2 = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss) # Manually calculate the general equilibrium Jacobian J_firm = firm.jac(ss, shock_list=['K', 'Z']) J_ha = household.jac(ss, T=T, shock_list=['r', 'w']) J_curlyK_K = J_ha['A']['r'] @ J_firm['r']['K'] + J_ha['A']['w'] @ J_firm['w']['K'] J_curlyK_Z = J_ha['A']['r'] @ J_firm['r']['Z'] + J_ha['A']['w'] @ J_firm['w']['Z'] - J = {**J_firm, 'curlyK': {'K': J_curlyK_K, 'Z': J_curlyK_Z}} - H_K = J['curlyK']['K'] - np.eye(T) - H_Z = J['curlyK']['Z'] + J_curlyK = {'curlyK': {'K': J_curlyK_K, 'Z': J_curlyK_Z}} + + H_K = J_curlyK['curlyK']['K'] - np.eye(T) + H_Z = J_curlyK['curlyK']['Z'] + G = {'K': -np.linalg.solve(H_K, H_Z)} # H_K^(-1)H_Z - G['r'] = J['r']['Z'] + J['r']['K'] @ G['K'] - G['w'] = J['w']['Z'] + J['w']['K'] @ G['K'] - G['Y'] = J['Y']['Z'] + J['Y']['K'] @ G['K'] + G['r'] = J_firm['r']['Z'] + J_firm['r']['K'] @ G['K'] + G['w'] = J_firm['w']['Z'] + J_firm['w']['K'] @ G['K'] + G['Y'] = J_firm['Y']['Z'] + J_firm['Y']['K'] @ G['K'] G['C'] = J_ha['C']['r'] @ G['r'] + J_ha['C']['w'] @ G['w'] for o in G: @@ -37,19 +40,19 @@ def test_hank_jac(one_asset_hank_model): T = 10 # Automatically calculate the general equilibrium Jacobian - G2 = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G2 = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss) # Manually calculate the general equilibrium Jacobian - curlyJs, required = jacobian.curlyJ_sorted(blocks, unknowns+exogenous, ss, T) - J_curlyH_U = jacobian.forward_accumulate(curlyJs, unknowns, targets, required) - J_curlyH_Z = jacobian.forward_accumulate(curlyJs, exogenous, targets, required) + curlyJs, required = curlyJ_sorted(blocks, unknowns+exogenous, ss, T) + J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) + J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) H_U = J_curlyH_U[targets, unknowns].pack(T) H_Z = J_curlyH_Z[targets, exogenous].pack(T) - G_U = jacobian.JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) + G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) curlyJs = [G_U] + curlyJs outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - G = jacobian.forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) + G = forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) for o in G: for i in G[o]: diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 35937ed..db9b25a 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -1,7 +1,10 @@ +"""Test SimpleBlock functionality""" + from sequence_jacobian import simple, utilities import numpy as np import pytest + @simple def F(K, L, Z, alpha): Y = Z * K(-1)**alpha * L**(1-alpha) diff --git a/tests/base/test_steady_state.py b/tests/base/test_steady_state.py index 7131a5c..dbc979c 100644 --- a/tests/base/test_steady_state.py +++ b/tests/base/test_steady_state.py @@ -2,7 +2,6 @@ import numpy as np -import sequence_jacobian as sj from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index df22b15..cba5473 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -3,19 +3,18 @@ import numpy as np import copy -from sequence_jacobian import two_asset, nonlinear, jacobian +from sequence_jacobian import two_asset, nonlinear, get_G, get_H_U from sequence_jacobian import utilities as utils # TODO: Figure out a more robust way to check similarity of the linear and non-linear solution. # As of now just checking that the tolerance for difference (by infinity norm) is below a manually checked threshold - def test_rbc_td(rbc_model): blocks, exogenous, unknowns, targets, ss = rbc_model T, impact, rho, news = 30, 0.01, 0.8, 10 - G = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss) dZ = np.empty((T, 2)) dZ[:, 0] = impact * ss['Z'] * rho**np.arange(T) @@ -37,8 +36,8 @@ def test_ks_td(krusell_smith_model): blocks, exogenous, unknowns, targets, ss = krusell_smith_model T = 30 - G = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss) for shock_size, tol in [(0.01, 7e-3), (0.1, 0.6)]: Z = ss['Z'] + shock_size * 0.8 ** np.arange(T) @@ -55,14 +54,14 @@ def test_hank_td(one_asset_hank_model): blocks, exogenous, unknowns, targets, ss = one_asset_hank_model T = 30 - G = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss, save=True) + G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss, save=True) rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) rstar = ss['r'] + drstar - H_U = jacobian.get_H_U(blocks, unknowns, targets, T, ss, use_saved=True) + H_U = get_H_U(blocks, unknowns, targets, T, ss, use_saved=True) H_U_factored = utils.misc.factor(H_U) td_nonlin = nonlinear.td_solve(ss, blocks, unknowns, targets, H_U_factored=H_U_factored, rstar=rstar, verbose=False) @@ -77,8 +76,8 @@ def test_two_asset_td(two_asset_hank_model): blocks, exogenous, unknowns, targets, ss = two_asset_hank_model T = 30 - G = jacobian.get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss, save=True) + G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, + targets=targets, T=T, ss=ss, save=True) for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: drstar = -0.0025 * 0.6 ** np.arange(T) @@ -106,8 +105,8 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_model): targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] T = 30 - G = jacobian.get_G(blocks, exogenous, unknowns, targets, T, ss=ss, save=True) - G_simple = jacobian.get_G(blocks_simple, exogenous, unknowns_simple, targets_simple, T, ss=ss, save=True) + G = get_G(blocks, exogenous, unknowns, targets, T, ss=ss, save=True) + G_simple = get_G(blocks_simple, exogenous, unknowns_simple, targets_simple, T, ss=ss, save=True) drstar = -0.0025 * 0.6 ** np.arange(T) From ad253a267f7aa9e56c0996ca84e85ec66eab89ed Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 10:48:16 -0600 Subject: [PATCH 010/288] Remove givedep kwarg and explicitly implement its functionality in drawdag for the single time it's used --- sequence_jacobian/visualization/draw_dag.py | 38 ++++++++------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/sequence_jacobian/visualization/draw_dag.py b/sequence_jacobian/visualization/draw_dag.py index 65665a3..2ad9a0a 100644 --- a/sequence_jacobian/visualization/draw_dag.py +++ b/sequence_jacobian/visualization/draw_dag.py @@ -7,7 +7,7 @@ warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ formatwarning_orig(message, category, filename, lineno, line='') -from .. import utilities as utils +from ..utilities.graph import ignore_helper_block_indices, topological_sort, construct_output_map, construct_dependency_graph from ..blocks.helper_block import HelperBlock @@ -28,9 +28,9 @@ """ from graphviz import Digraph - # TODO: Integrate the givedep and giveio functionality into the base block_sort functionality in SSJ + # TODO: Integrate the giveio functionality into the base block_sort functionality in SSJ # Enhanced block sort - def block_sort_enhanced(block_list, findrequired=False, givedep=False, giveio=False, ignore_helpers=True): + def block_sort_enhanced(block_list, findrequired=False, giveio=False, ignore_helpers=True): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort and also optionally return which outputs must be computed as inputs of later blocks. @@ -84,27 +84,18 @@ def block_sort_enhanced(block_list, findrequired=False, givedep=False, giveio=Fa if findrequired: required.add(i) - if givedep: - if findrequired: - if giveio: - return dep, required, inset, outset - else: - return dep, inset, outset - else: - return dep + if ignore_helpers: + dep_sorted = ignore_helper_block_indices(topological_sort(dep), block_list) else: - if ignore_helpers: - dep_sorted = utils.graph.ignore_helper_block_indices(utils.graph.topological_sort(dep), block_list) - else: - dep_sorted = utils.graph.topological_sort(dep) + dep_sorted = topological_sort(dep) - if findrequired: - if giveio: - return dep_sorted, required, inset, outset - else: - return dep_sorted, required + if findrequired: + if giveio: + return dep_sorted, required, inset, outset else: - return dep_sorted + return dep_sorted, required + else: + return dep_sorted def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, @@ -140,7 +131,7 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, # get sorted list of blocks block_list_sorted = [block_list[i] for i in topsorted] # Obtain the dependency list of the sorted set of blocks - dep_list_sorted = block_sort_enhanced(block_list_sorted, givedep=True) + dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted)) # Draw DAG dot = Digraph(comment='Model DAG') @@ -209,8 +200,7 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, # print(dot.source) if debug: - dep, required, inputs, outputs = block_sort_enhanced(block_list_sorted, findrequired=True, - givedep=False, giveio=True) + dep, required, inputs, outputs = block_sort_enhanced(block_list_sorted, findrequired=True, giveio=True) # Candidate targets: outputs that are not inputs to any block print("Candidate targets :") cand_targets = outputs.difference(required) From e0c77b489635af3edb1b42db9d58f3ade1799afe Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 11:19:33 -0600 Subject: [PATCH 011/288] Incorporate the functionality in block_sort_enhanced to return inputs and outputs into block_sort --- sequence_jacobian/utilities/graph.py | 59 ++++++++++--- sequence_jacobian/visualization/draw_dag.py | 96 +++------------------ 2 files changed, 61 insertions(+), 94 deletions(-) diff --git a/sequence_jacobian/utilities/graph.py b/sequence_jacobian/utilities/graph.py index cdc4ad3..66e3163 100644 --- a/sequence_jacobian/utilities/graph.py +++ b/sequence_jacobian/utilities/graph.py @@ -6,7 +6,7 @@ from ..blocks.solved_block import SolvedBlock -def block_sort(block_list, ignore_helpers=False, calibration=None): +def block_sort(block_list, ignore_helpers=False, calibration=None, return_io=False): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's @@ -32,17 +32,37 @@ def block_sort(block_list, ignore_helpers=False, calibration=None): calibration: `dict` or `None` An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles introduced by using HelperBlocks. Read above docstring for more detail + return_io: `bool` + A boolean indicating whether to return the full set of input and output arguments from `block_list` """ - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(block_list) + # TODO: Decide whether we want to break out the input and output argument tracking and return to + # a different function... currently it's very convenient to slot it into block_sort directly, but it + # does clutter up the function body + if return_io: + # step 1: map outputs to blocks for topological sort + outmap, outargs = construct_output_map(block_list, return_output_args=True) - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(block_list, outmap, calibration=calibration, ignore_helpers=ignore_helpers) - if ignore_helpers: - return ignore_helper_block_indices(topological_sort(dep), block_list) + # step 2: dependency graph for topological sort and input list + dep, inargs = construct_dependency_graph(block_list, outmap, return_input_args=True, + calibration=calibration, ignore_helpers=ignore_helpers) + + if ignore_helpers: + return ignore_helper_block_indices(topological_sort(dep), block_list), inargs, outargs + else: + return topological_sort(dep), inargs, outargs else: - return topological_sort(dep) + # step 1: map outputs to blocks for topological sort + outmap = construct_output_map(block_list) + + # step 2: dependency graph for topological sort and input list + dep = construct_dependency_graph(block_list, outmap, + calibration=calibration, ignore_helpers=ignore_helpers) + + if ignore_helpers: + return ignore_helper_block_indices(topological_sort(dep), block_list) + else: + return topological_sort(dep) def topological_sort(dep, names=None): @@ -76,7 +96,7 @@ def ignore_helper_block_indices(topsorted, blocks): return [i for i in topsorted if not isinstance(blocks[i], HelperBlock)] -def construct_output_map(block_list, ignore_helpers=False): +def construct_output_map(block_list, ignore_helpers=False, return_output_args=False): """Construct a map of outputs to the indices of the blocks that produce them. block_list: `list` @@ -84,8 +104,12 @@ def construct_output_map(block_list, ignore_helpers=False): ignore_helpers: `bool` A boolean indicating whether to account for/return the indices of HelperBlocks contained in block_list Set to true when sorting for td and jac calculations + return_output_args: `bool` + A boolean indicating whether to track and return the full set of output arguments of all of the blocks + in `block_list` """ outmap = dict() + outargs = set() for num, block in enumerate(block_list): if ignore_helpers and isinstance(block, HelperBlock): continue @@ -109,12 +133,17 @@ def construct_output_map(block_list, ignore_helpers=False): # a given output, such that the dependency graph is constructed on the standard blocks, where possible if o not in outmap or (o in outmap and not isinstance(block, HelperBlock)): outmap[o] = num + if return_output_args: + outargs.add(o) else: continue - return outmap + if return_output_args: + return outmap, outargs + else: + return outmap -def construct_dependency_graph(block_list, outmap, calibration=None, ignore_helpers=False): +def construct_dependency_graph(block_list, outmap, ignore_helpers=False, calibration=None, return_input_args=False): """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where this set is the set of blocks that the key block is dependent on. @@ -125,6 +154,7 @@ def construct_dependency_graph(block_list, outmap, calibration=None, ignore_help if calibration is None: calibration = {} dep = {num: set() for num in range(len(block_list))} + inargs = set() for num, block in enumerate(block_list): if ignore_helpers and isinstance(block, HelperBlock): continue @@ -133,6 +163,8 @@ def construct_dependency_graph(block_list, outmap, calibration=None, ignore_help else: inputs = set(i for o in block for i in block[o]) for i in inputs: + if return_input_args: + inargs.add(i) # Each potential input to a given block will either be 1) output by another block, # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into # the steady-state computation via the `calibration' dict. @@ -143,7 +175,10 @@ def construct_dependency_graph(block_list, outmap, calibration=None, ignore_help # dependency, if it were not for this resolution. if i in outmap and not (i in calibration and isinstance(block, HelperBlock)): dep[num].add(outmap[i]) - return dep + if return_input_args: + return dep, inargs + else: + return dep def find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=False): diff --git a/sequence_jacobian/visualization/draw_dag.py b/sequence_jacobian/visualization/draw_dag.py index 2ad9a0a..1b4d445 100644 --- a/sequence_jacobian/visualization/draw_dag.py +++ b/sequence_jacobian/visualization/draw_dag.py @@ -7,8 +7,8 @@ warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ formatwarning_orig(message, category, filename, lineno, line='') -from ..utilities.graph import ignore_helper_block_indices, topological_sort, construct_output_map, construct_dependency_graph -from ..blocks.helper_block import HelperBlock +from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph,\ + find_outputs_that_are_intermediate_inputs # Implement DAG drawing functions as "soft" dependencies to not enforce the installation of graphviz, since @@ -28,77 +28,8 @@ """ from graphviz import Digraph - # TODO: Integrate the giveio functionality into the base block_sort functionality in SSJ - # Enhanced block sort - def block_sort_enhanced(block_list, findrequired=False, giveio=False, ignore_helpers=True): - """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort and also - optionally return which outputs must be computed as inputs of later blocks. - Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's - inferred) that indicate their aggregate inputs and outputs""" - - # step 1: map outputs to blocks for topological sort - outmap = dict() - outset = set() - - for num, block in enumerate(block_list): - if ignore_helpers and isinstance(block, HelperBlock): - continue - else: - if hasattr(block, 'outputs'): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: - if o in outmap: - raise ValueError(f'{o} is output twice') - outmap[o] = num - if giveio: - outset.add(o) - - # step 2: dependency graph for topological sort and input list - if ignore_helpers: - dep = {num: set() for num in range(len(block_list))} - else: - dep = {num: set() for num in range(len(block_list))} - inset = set() - if findrequired: - required = set() - for num, block in enumerate(block_list): - if ignore_helpers and isinstance(block, HelperBlock): - continue - else: - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - - for i in inputs: - if giveio: - inset.add(i) - if i in outmap: - dep[num].add(outmap[i]) - if findrequired: - required.add(i) - - if ignore_helpers: - dep_sorted = ignore_helper_block_indices(topological_sort(dep), block_list) - else: - dep_sorted = topological_sort(dep) - - if findrequired: - if giveio: - return dep_sorted, required, inset, outset - else: - return dep_sorted, required - else: - return dep_sorted - - - def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, + def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_helpers=True, calibration=None, showdag=False, debug=False, leftright=False, filename='modeldag'): """ Visualizes a Directed Acyclic Graph (DAG) of a set of blocks, exogenous variables, unknowns, and targets @@ -111,6 +42,11 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, Unknown variables, to be represented on DAG targets: `list` (optional) Target variables, to be represented on DAG + ignore_helpers: `bool` + A boolean indicating whether to also draw HelperBlocks contained in block_list into the DAG + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using HelperBlocks. Read `block_sort` docstring for more detail showdag: `bool` If True, export and plot pdf file. If false, export png file and do not plot debug: `bool` @@ -127,11 +63,12 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, targets = [] if targets is None else targets # obtain the topological sort - topsorted = block_sort_enhanced(block_list) + topsorted = block_sort(block_list, ignore_helpers=ignore_helpers, calibration=calibration) # get sorted list of blocks block_list_sorted = [block_list[i] for i in topsorted] # Obtain the dependency list of the sorted set of blocks - dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted)) + dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted), + ignore_helpers=ignore_helpers, calibration=calibration) # Draw DAG dot = Digraph(comment='Model DAG') @@ -200,7 +137,9 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, # print(dot.source) if debug: - dep, required, inputs, outputs = block_sort_enhanced(block_list_sorted, findrequired=True, giveio=True) + dep, inputs, outputs = block_sort(block_list_sorted, return_io=True, ignore_helpers=ignore_helpers, + calibration=calibration) + required = find_outputs_that_are_intermediate_inputs(block_list_sorted, ignore_helpers=ignore_helpers) # Candidate targets: outputs that are not inputs to any block print("Candidate targets :") cand_targets = outputs.difference(required) @@ -224,13 +163,6 @@ def inspect_solved(block_list): if hasattr(block, 'block_list'): draw_solved(block, filename=str(block.block_list[0].f.__name__)) except ImportError: - def block_sort_enhanced(*args, **kwargs): - warnings.warn("\nThe package `graphviz` has not yet been installed. \n" - "`block_sort_enhanced` is meant to aid in DAG visualization. \n" - "For now, please use `block_sort` instead.") - pass - - def draw_dag(*args, **kwargs): warnings.warn("\nAttempted to use `draw_dag` when the package `graphviz` has not yet been installed. \n" "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" From ca0e32e2913d1257971be03ff1fd284f7dce2ba4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 11:35:29 -0600 Subject: [PATCH 012/288] Add .name attribute to blocks --- sequence_jacobian/blocks/combined_block.py | 6 ++++-- sequence_jacobian/blocks/helper_block.py | 1 + sequence_jacobian/blocks/het_block.py | 1 + sequence_jacobian/blocks/simple_block.py | 1 + sequence_jacobian/blocks/solved_block.py | 12 +++++++----- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 20fccfa..307a380 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -13,9 +13,11 @@ def combine(*args): class CombinedBlock: - def __init__(self, *args, name=None): + def __init__(self, *args, name=""): self.blocks = args - if name is not None: + if not name: + self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" + else: self.name = name # Find all outputs (including those used as intermediary inputs) diff --git a/sequence_jacobian/blocks/helper_block.py b/sequence_jacobian/blocks/helper_block.py index bf548db..12b87ad 100644 --- a/sequence_jacobian/blocks/helper_block.py +++ b/sequence_jacobian/blocks/helper_block.py @@ -14,6 +14,7 @@ class HelperBlock: def __init__(self, f): self.f = f + self.name = f.__name__ self.input_list = utils.misc.input_list(f) self.output_list = utils.misc.output_list(f) self.inputs = set(self.input_list) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 94cb48a..0af77f7 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -49,6 +49,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non Currently, we only support up to two policy variables. """ + self.name = back_step_fun.__name__ # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 72a3d1b..c797a4f 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -28,6 +28,7 @@ def production(Z, K, L, alpha): def __init__(self, f): self.f = f + self.name = f.__name__ self.input_list = misc.input_list(f) self.output_list = misc.output_list(f) self.inputs = set(self.input_list) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 5f422d7..47eca6a 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -4,20 +4,21 @@ from ..blocks.simple_block import simple -def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}): +def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: - as @solved(unknowns=..., targets=...) decorator on a single SimpleBlock - as function solved(blocklist=..., unknowns=..., targets=...) where blocklist can be any list of blocks """ - if block_list: + if not name: + name = f"{block_list[0].name}_to_{block_list[-1].name}_solved" # ordinary call, not as decorator - return SolvedBlock(block_list, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + return SolvedBlock(block_list, name, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) else: # call as decorator, return function of function def singleton_solved_block(f): - return SolvedBlock([simple(f)], unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + return SolvedBlock([simple(f)], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) return singleton_solved_block @@ -34,8 +35,9 @@ class SolvedBlock: nonlinear transition path such that all internal targets of the mini SHADE model are zero. """ - def __init__(self, block_list, unknowns, targets, solver=None, solver_kwargs={}): + def __init__(self, block_list, name, unknowns, targets, solver=None, solver_kwargs={}): self.block_list = block_list + self.name = name self.unknowns = unknowns self.targets = targets self.solver = solver From 22df18bc0f473428d1b964b53c05959e76e45ee3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 17:16:49 -0600 Subject: [PATCH 013/288] Make all Block objects consistent and return an actual "empty" JacobianDict, if the shocks are not an input to the block --- sequence_jacobian/blocks/het_block.py | 7 +++++-- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/blocks/solved_block.py | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 0af77f7..ea78902 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -401,10 +401,13 @@ def jac(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair - F = {o.capitalize(): {} for o in output_list} - J = {o.capitalize(): {} for o in output_list} + F, J = {}, {} for o in output_list: for i in relevant_shocks: + if o.capitalize() not in F: + F[o.capitalize()] = {} + if o.capitalize() not in J: + J[o.capitalize()] = {} F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index c797a4f..dec75a7 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -113,7 +113,7 @@ def jac(self, ss, T=None, shock_list=[]): # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict if not relevant_shocks: - return {} + return JacobianDict({}) else: invertedJ = {shock_name: {} for shock_name in relevant_shocks} diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 47eca6a..b4f28a6 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,5 +1,6 @@ from .. import nonlinear from ..jacobian.drivers import get_G, get_G_asymptotic +from ..jacobian.classes import JacobianDict from ..steady_state import steady_state from ..blocks.simple_block import simple @@ -65,9 +66,12 @@ def td(self, ss, monotonic=False, returnindividual=False, verbose=False, **kwarg def jac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False): relevant_shocks = [i for i in self.inputs if i in shock_list] - # H_U_factored caching could be helpful here too - return get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, - T, ss, output_list, save=save, use_saved=use_saved) + if not relevant_shocks: + return JacobianDict({}) + else: + # H_U_factored caching could be helpful here too + return get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, + T, ss, output_list, save=save, use_saved=use_saved) def ajac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False, Tpost=None): relevant_shocks = [i for i in self.inputs if i in shock_list] From deaf8ee53d5c9f06d1f42d3a078ddaf58009b64b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 20:38:52 -0600 Subject: [PATCH 014/288] Move numeric_primitive to utilities/misc --- sequence_jacobian/blocks/simple_block.py | 10 +++++----- .../blocks/support/simple_displacement.py | 9 +-------- sequence_jacobian/utilities/misc.py | 8 ++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index dec75a7..ae3d9b7 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -1,6 +1,6 @@ import numpy as np -from .support.simple_displacement import ignore, numeric_primitive, Displace, AccumulatedDerivative +from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc @@ -41,9 +41,9 @@ def _output_in_ss_format(self, *args, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" if len(self.output_list) > 1: - return tuple([numeric_primitive(o) for o in self.f(*args, **kwargs)]) + return tuple([misc.numeric_primitive(o) for o in self.f(*args, **kwargs)]) else: - return numeric_primitive(self.f(*args, **kwargs)) + return misc.numeric_primitive(self.f(*args, **kwargs)) def ss(self, *args, **kwargs): # Wrap args and kwargs in Ignore/IgnoreVector classes to be passed into the function "f" @@ -67,10 +67,10 @@ def _output_in_td_format(self, **kwargs_new): if len(self.output_list) > 1: # Because we know at least one of the outputs in `out` must be of length T T = np.max([np.size(o) for o in out]) - out_unif_dim = [np.full(T, numeric_primitive(o)) if np.isscalar(o) else numeric_primitive(o) for o in out] + out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else misc.numeric_primitive(o) for o in out] return dict(zip(self.output_list, misc.make_tuple(out_unif_dim))) else: - return dict(zip(self.output_list, misc.make_tuple(numeric_primitive(out)))) + return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) def td(self, ss, **kwargs): kwargs_new = {} diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/sequence_jacobian/blocks/support/simple_displacement.py index 3d63da6..87ee155 100644 --- a/sequence_jacobian/blocks/support/simple_displacement.py +++ b/sequence_jacobian/blocks/support/simple_displacement.py @@ -4,6 +4,7 @@ import numbers from warnings import warn +from ...utilities.misc import numeric_primitive def ignore(x): if isinstance(x, int): @@ -669,14 +670,6 @@ def compute_l(i, m, j, n): return max(m, n + i) -def numeric_primitive(instance): - # If it is already a primitive, just return it - if type(instance) in {int, float, np.ndarray}: - return instance - else: - return instance.real if np.isscalar(instance) else instance.base - - # TODO: This needs its own unit test def vectorize_func_over_time(func, *args): """In `args` some arguments will be Displace objects and others will be Ignore/IgnoreVector objects. diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index a9dbef9..6a693a3 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -34,6 +34,14 @@ def output_list(f): return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') +def numeric_primitive(instance): + # If it is already a primitive, just return it + if type(instance) in {int, float, np.ndarray}: + return instance + else: + return instance.real if np.isscalar(instance) else instance.base + + def demean(x): return x - x.sum()/x.size From 16c2d51f0197351ccbc529a0c8cc96215b6d5fd3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 20:47:02 -0600 Subject: [PATCH 015/288] Alter SimpleBlock and HelperBlock .ss methods to return dicts instead of tuples for consistency across blocks --- sequence_jacobian/blocks/helper_block.py | 16 ++++++++++++---- sequence_jacobian/blocks/simple_block.py | 4 ++-- sequence_jacobian/steady_state.py | 15 ++++++--------- tests/base/test_simple_block.py | 2 +- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/sequence_jacobian/blocks/helper_block.py b/sequence_jacobian/blocks/helper_block.py index 12b87ad..6250860 100644 --- a/sequence_jacobian/blocks/helper_block.py +++ b/sequence_jacobian/blocks/helper_block.py @@ -1,6 +1,6 @@ """HelperBlock class and @helper decorator to generate it""" -from .. import utilities as utils +from ..utilities import misc def helper(f): @@ -15,17 +15,25 @@ class HelperBlock: def __init__(self, f): self.f = f self.name = f.__name__ - self.input_list = utils.misc.input_list(f) - self.output_list = utils.misc.output_list(f) + self.input_list = misc.input_list(f) + self.output_list = misc.output_list(f) self.inputs = set(self.input_list) self.outputs = set(self.output_list) def __repr__(self): return f"" + def _output_in_ss_format(self, *args, **kwargs): + """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single + numeric primitive, as opposed to Ignore/IgnoreVector objects""" + if len(self.output_list) > 1: + return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(*args, **kwargs)])) + else: + return dict(zip(self.output_list, [misc.numeric_primitive(self.f(*args, **kwargs))])) + # Currently does not use any of the machinery in SimpleBlock to deal with time displacements and hence # can handle non-scalar inputs. def ss(self, *args, **kwargs): args = [x for x in args] kwargs = {k: v for k, v in kwargs.items()} - return self.f(*args, **kwargs) + return self._output_in_ss_format(*args, **kwargs) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index ae3d9b7..b32f640 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -41,9 +41,9 @@ def _output_in_ss_format(self, *args, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" if len(self.output_list) > 1: - return tuple([misc.numeric_primitive(o) for o in self.f(*args, **kwargs)]) + return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(*args, **kwargs)])) else: - return misc.numeric_primitive(self.f(*args, **kwargs)) + return dict(zip(self.output_list, [misc.numeric_primitive(self.f(*args, **kwargs))])) def ss(self, *args, **kwargs): # Wrap args and kwargs in Ignore/IgnoreVector classes to be passed into the function "f" diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state.py index 44ea816..1598096 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state.py @@ -147,6 +147,7 @@ def compute_target_values(targets, potential_args): else: return target_values + # Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **kwargs): """ @@ -160,15 +161,11 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol # Simple and HetBlocks require different handling of block.ss() output since # SimpleBlocks return a tuple of un-labeled arguments, whereas HetBlocks return dictionaries - if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock): - output_args = utils.misc.make_tuple(block.ss(**input_args, **kwargs)) - outputs = {o: output_args[i] for i, o in enumerate(block.output_list)} - else: # assume it's a HetBlock or a SolvedBlock - if isinstance(block, HetBlock): # since .ss for SolvedBlocks calls the steady_state driver function - outputs = block.ss(**input_args, **kwargs) - else: # since .ss for SolvedBlocks calls the steady_state driver function - outputs = block.ss(**input_args, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) + if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock) or isinstance(block, HetBlock): + outputs = block.ss(**input_args, **kwargs) + else: # since .ss for SolvedBlocks calls the steady_state driver function + outputs = block.ss(**input_args, consistency_check=consistency_check, + ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) return outputs diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index db9b25a..6714476 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -34,7 +34,7 @@ def test_block_consistency(block, ss): """Make sure ss, td, and jac methods are all consistent with each other. Requires that all inputs of simple block allow calculating Jacobians""" # get ss output - ss_results = dict(zip(block.output_list, utilities.misc.make_tuple(block.ss(*ss)))) + ss_results = block.ss(*ss) # now if we put in constant inputs, td should give us the same! ss = dict(zip(block.input_list, ss)) From 375cc179777782f075ca71aeceb72fbb6647da9b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 4 Feb 2021 21:44:41 -0600 Subject: [PATCH 016/288] Implement devtool sub-package, including modules (some placeholders) DAG analysis tools, code deprecation and upgrade support --- sequence_jacobian/devtools/__init__.py | 3 + sequence_jacobian/devtools/analysis.py | 104 ++++++++++++++++++ sequence_jacobian/devtools/debug.py | 44 ++++++++ sequence_jacobian/devtools/deprecate.py | 5 + .../devtools.py => devtools/upgrade.py} | 12 +- sequence_jacobian/jacobian/classes.py | 2 +- sequence_jacobian/visualization/draw_dag.py | 18 +-- 7 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 sequence_jacobian/devtools/__init__.py create mode 100644 sequence_jacobian/devtools/analysis.py create mode 100644 sequence_jacobian/devtools/debug.py create mode 100644 sequence_jacobian/devtools/deprecate.py rename sequence_jacobian/{utilities/devtools.py => devtools/upgrade.py} (69%) diff --git a/sequence_jacobian/devtools/__init__.py b/sequence_jacobian/devtools/__init__.py new file mode 100644 index 0000000..b9332f6 --- /dev/null +++ b/sequence_jacobian/devtools/__init__.py @@ -0,0 +1,3 @@ +"""Tools for debugging, developing, and deprecating code""" + +from . import analysis, deprecate, upgrade diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py new file mode 100644 index 0000000..5d6255a --- /dev/null +++ b/sequence_jacobian/devtools/analysis.py @@ -0,0 +1,104 @@ +"""Low-level tools/classes for analyzing sequence-jacobian model DAGs to support debugging""" + +import numpy as np +import xarray as xr + +from ..utilities import graph + + +class BlockIONetwork: + """ + A 3-d axis-labeled DataArray (blocks x inputs x outputs), which allows for the analysis of the input-output + structure of a DAG. + """ + def __init__(self, blocks): + topsorted, inset, outset = graph.block_sort(blocks, return_io=True) + self.blocks = {b.name: b for b in blocks} + self.blocks_names = list(self.blocks.keys()) + self.blocks_as_list = list(self.blocks.values()) + self.var_names = list(inset.union(outset)) + self.darray = xr.DataArray(np.zeros((len(blocks), len(self.var_names), len(self.var_names))), + coords=[self.blocks_names, self.var_names, self.var_names], + dims=["blocks", "inputs", "outputs"]) + + def __repr__(self): + return self.darray.__repr__() + + # User-facing "print" methods + def print_block_links(self, block_name): + print(f" {block_name}") + print(" " + "-" * (len(block_name))) + + link_inds = np.nonzero(self._subset_by_block(block_name)).data + for i in range(np.shape(link_inds)[0]): + i_ind, o_ind = link_inds[:, i] + i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) + o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) + print(f" {i_var} -> {o_var}") + + def print_var_links(self, var_name): + pass + + # User-facing "analysis" methods + def record_input_variable_paths(self, inputs_to_be_recorded, block_input_args): + """ + Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG + + Parameters + ---------- + inputs_to_be_recorded: `list(str)` + A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork + block_input_args: `dict` + A dict of variable/parameter names and values (typically from the steady state of the model) on which, + a block can perform a valid evaluation + """ + block_inds_sorted = graph.block_sort(self.blocks_as_list) + for input_var in inputs_to_be_recorded: + all_input_vars = set(input_var) + for ib in block_inds_sorted: + ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} + # This extra step is needed because some arguments required for calling .jac on + # HetBlock and SolvedBlock are not a part of .inputs + ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) + io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) + if io_links: + self._record_io_links(self.blocks_names[ib], io_links) + # Need to also track the paths of outputs which could be intermediate inputs further down the DAG + all_input_vars = all_input_vars.union(set(io_links.keys())) + + # Analysis support methods + def _subset_by_block(self, block_name): + return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] + + def _subset_by_var(self, var_name): + return self.darray.loc[[b.name for b in self.blocks.values() if var_name in b.inputs], [var_name], :] + + def _record_io_links(self, block_name, io_links): + for o, i in io_links.items(): + self.darray.loc[block_name, i, o] = 1. + + +def find_io_links(block, input_args, block_input_args): + """ + For a given `block`, see which output arguments the input argument `input_args` affects + + Parameters + ---------- + block: `Block` object + One of the various kinds of `Block` objects (`SimpleBlock`, `HetBlock`, etc.) + input_args: `str` or `list(str)` + The input arguments, whose paths through the block to the output variables we want to see + block_input_args: `dict{str: num}` + The rest of the input arguments required to evaluate the block's Jacobian + + Returns + ------- + links: `dict{str: list(str)}` + A dict with *output arguments* as keys, and the input arguments that affect it as values + """ + J = block.jac(ss=block_input_args, T=2, shock_list=input_args) + links = {} + for o in J.outputs: + links[o] = list(J.nesteddict[o].keys()) + return links + diff --git a/sequence_jacobian/devtools/debug.py b/sequence_jacobian/devtools/debug.py new file mode 100644 index 0000000..41b010a --- /dev/null +++ b/sequence_jacobian/devtools/debug.py @@ -0,0 +1,44 @@ +"""Top-level tools to help users debug their sequence-jacobian code""" + +from ..utilities import graph + + +def ensure_computability(blocks, calibration, unknowns, verbose=False): + ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=verbose) + # TODO: Ensure that all of the unknowns map in some way to at least one target each + + +# To ensure that no input argument that is required for one of the blocks to evaluate is missing +def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False): + variables_accounted_for = set(unknowns.keys()).union(set(calibration.keys())) + all_inputs = set().union(*[b.inputs for b in blocks]) + required = graph.find_outputs_that_are_intermediate_inputs(blocks) + non_computed_inputs = all_inputs.difference(required) + + variables_unaccounted_for = non_computed_inputs.difference(variables_accounted_for) + if variables_unaccounted_for: + raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" + f"parameters: {variables_unaccounted_for}") + if verbose: + print("This DAG accounts for all inputs variables.") + + +def ensure_unknowns_and_targets_valid(blocks, unknowns, targets, verbose=False): + pass + + +def find_candidate_unknowns_and_targets(block_list, verbose=False, ignore_helpers=True, calibration=None): + dep, inputs, outputs = graph.block_sort(block_list, return_io=True, ignore_helpers=ignore_helpers, + calibration=calibration) + required = graph.find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=ignore_helpers) + + # Candidate exogenous and unknowns (also includes parameters): inputs that are not outputs of any block + # Candidate targets: outputs that are not inputs to any block + cand_xu = inputs.difference(required) + cand_targets = outputs.difference(required) + + if verbose: + print(f"Candidate exogenous/unknowns: {cand_xu}\n" + f"Candidate targets: {cand_targets}") + + return cand_xu, cand_targets diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py new file mode 100644 index 0000000..2784653 --- /dev/null +++ b/sequence_jacobian/devtools/deprecate.py @@ -0,0 +1,5 @@ +"""Tools for deprecating older SSJ code conventions in favor of newer conventions""" + +# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, by temporarily +# providing support for old conventions via deprecated methods, providing time to allow for a seamless upgrade +# to newer versions sequence-jacobian. diff --git a/sequence_jacobian/utilities/devtools.py b/sequence_jacobian/devtools/upgrade.py similarity index 69% rename from sequence_jacobian/utilities/devtools.py rename to sequence_jacobian/devtools/upgrade.py index 35671f2..a547195 100644 --- a/sequence_jacobian/utilities/devtools.py +++ b/sequence_jacobian/devtools/upgrade.py @@ -1,10 +1,17 @@ -"""Useful functions for debugging SSJ code""" +"""Tools for upgrading from older SSJ code conventions""" + +# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, and who +# may want additional support/tools for ensuring that their attempts to upgrade to use newer versions of +# sequence-jacobian has been successfully. import numpy as np -# Tools for upgrading from older SSJ code conventions def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): + """ + This code is meant to provide a quick comparison of `ss_ref` the reference steady state dict from old code, and + `ss_comp` the steady state computed from the newer code. + """ if name_map is None: name_map = {} @@ -30,4 +37,3 @@ def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): diff_keys = ss_ref_incl_mapped.symmetric_difference(ss_comp_incl_mapped) if diff_keys: print(f"The keys present in only one of the two steady state dicts are {diff_keys}") - diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index a1bb843..b42cc9d 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -333,7 +333,7 @@ def __getitem__(self, x): raise ValueError(f'Tried to get impermissible item {x}') def get(self, *args, **kwargs): - # this is for compatibilty, not a huge fan + # this is for compatibility, not a huge fan return self.nesteddict.get(*args, **kwargs) def update(self, J): diff --git a/sequence_jacobian/visualization/draw_dag.py b/sequence_jacobian/visualization/draw_dag.py index 1b4d445..7b81e9f 100644 --- a/sequence_jacobian/visualization/draw_dag.py +++ b/sequence_jacobian/visualization/draw_dag.py @@ -30,7 +30,7 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_helpers=True, calibration=None, - showdag=False, debug=False, leftright=False, filename='modeldag'): + showdag=False, leftright=False, filename='modeldag'): """ Visualizes a Directed Acyclic Graph (DAG) of a set of blocks, exogenous variables, unknowns, and targets @@ -49,8 +49,6 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_hel introduced by using HelperBlocks. Read `block_sort` docstring for more detail showdag: `bool` If True, export and plot pdf file. If false, export png file and do not plot - debug: `bool` - If True, returns list of candidate unknown and targets leftright: `bool` If True, plots DAG from left to right instead of top to bottom @@ -136,20 +134,6 @@ def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_hel dot.render('dagexport/' + filename, format='png', cleanup=True) # print(dot.source) - if debug: - dep, inputs, outputs = block_sort(block_list_sorted, return_io=True, ignore_helpers=ignore_helpers, - calibration=calibration) - required = find_outputs_that_are_intermediate_inputs(block_list_sorted, ignore_helpers=ignore_helpers) - # Candidate targets: outputs that are not inputs to any block - print("Candidate targets :") - cand_targets = outputs.difference(required) - print(cand_targets) - # Candidate exogenous and unknowns (also includes parameters) - # inputs that are not outputs of any block - print("Candidate exogenous/unknowns :") - cand_xu = inputs.difference(required) - print(cand_xu) - def draw_solved(solvedblock, filename='solveddag'): # Inspects a solved block by drawing its DAG From d8ed0ab7f04ddb5b056b70fae329fdf2989ec0c9 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 10:19:38 -0600 Subject: [PATCH 017/288] Refine _subset_by_vars to be able to handle both individual string inputs and list of string inputs --- sequence_jacobian/devtools/analysis.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index 5d6255a..aec1119 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -2,6 +2,7 @@ import numpy as np import xarray as xr +from collections.abc import Iterable from ..utilities import graph @@ -36,8 +37,6 @@ def print_block_links(self, block_name): o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) print(f" {i_var} -> {o_var}") - def print_var_links(self, var_name): - pass # User-facing "analysis" methods def record_input_variable_paths(self, inputs_to_be_recorded, block_input_args): @@ -70,8 +69,12 @@ def record_input_variable_paths(self, inputs_to_be_recorded, block_input_args): def _subset_by_block(self, block_name): return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] - def _subset_by_var(self, var_name): - return self.darray.loc[[b.name for b in self.blocks.values() if var_name in b.inputs], [var_name], :] + def _subset_by_vars(self, vars_names): + if isinstance(vars_names, Iterable): + return self.darray.loc[[b.name for b in self.blocks.values() if np.any(v in b.inputs for v in vars_names)], + vars_names, :] + else: + return self.darray.loc[[b.name for b in self.blocks.values() if vars_names in b.inputs], vars_names, :] def _record_io_links(self, block_name, io_links): for o, i in io_links.items(): From df7e375cc8b3151b87c04390bccd2489f75f92fb Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 16:07:19 -0600 Subject: [PATCH 018/288] Implement print_var_links method for BlockIONetwork class --- sequence_jacobian/__init__.py | 2 +- sequence_jacobian/devtools/analysis.py | 87 +++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 68eca9b..65ffb51 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -1,6 +1,6 @@ """Public-facing objects.""" -from . import asymptotic, determinacy, estimation, jacobian, nonlinear, utilities +from . import asymptotic, determinacy, estimation, jacobian, nonlinear, utilities, devtools from .models import rbc, krusell_smith, hank, two_asset diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index aec1119..6a1a507 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -27,8 +27,8 @@ def __repr__(self): # User-facing "print" methods def print_block_links(self, block_name): - print(f" {block_name}") - print(" " + "-" * (len(block_name))) + print(f" Links in {block_name}") + print(" " + "-" * (len(f" Links in {block_name}"))) link_inds = np.nonzero(self._subset_by_block(block_name)).data for i in range(np.shape(link_inds)[0]): @@ -37,6 +37,50 @@ def print_block_links(self, block_name): o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) print(f" {i_var} -> {o_var}") + def print_var_links(self, var_name, calibration=None, ignore_helpers=True): + print(f" Links for {var_name}") + print(" " + "-" * (len(f" Links for {var_name}"))) + + # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those + # `outputs` and instantiate the initial list of links + link_inds = np.nonzero(self._subset_by_vars(var_name).data) + links = [[var_name] for i in range(len(link_inds[0]))] + + block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, + ignore_helpers=ignore_helpers) + required = graph.find_outputs_that_are_intermediate_inputs(self.blocks_as_list, ignore_helpers=ignore_helpers) + intermediates = set() + for ib in block_inds_sorted: + # Note: This block is ordered before the bottom block of code since the intermediate outputs from a block + # `ib` do not need to be checked as inputs to that same block `ib`, only the subsequent blocks + if intermediates: + intm_link_inds = np.nonzero(self._subset_by_vars(list(intermediates)).data) + # If there are `intermediate` inputs that have been recorded, we need to find the *indirect* links + for iil, iilb in enumerate(intm_link_inds[0]): + # Check if those inputs are inputs to block `ib` + if ib == iilb: + # If so, repeat the logic from below, where you find the input-output var link + o_var = str(self._subset_by_vars(list(intermediates)).coords["outputs"][intm_link_inds[2][iil]].data) + intm_i_var = str(self._subset_by_vars(list(intermediates)).coords["inputs"][intm_link_inds[1][iil]].data) + + # And add it to the set of all links, recording this new links' output if it hasn't appeared + # before and if it is an intermediate input + links.append([intm_i_var, o_var]) + if o_var in required: + intermediates = intermediates.union(o_var) + + # Check if `var_name` enters into that block as an input, and if so add the link between it and the output + # it directly affects, recording that output as an intermediate input if needed + # Note: link_inds' 0-th row indicates the blocks and 1st row indicates the outputs + for il, ilb in enumerate(link_inds[0]): + if ib == ilb: + o_var = str(self._subset_by_vars(var_name).coords["outputs"][link_inds[1][il]].data) + links[il].append(o_var) + if o_var in required: + intermediates = intermediates.union(o_var) + + for link_c in _compose_dyad_links(links): + print(" -> ".join(link_c)) # User-facing "analysis" methods def record_input_variable_paths(self, inputs_to_be_recorded, block_input_args): @@ -81,6 +125,44 @@ def _record_io_links(self, block_name, io_links): self.darray.loc[block_name, i, o] = 1. +def _compose_dyad_links(links): + links_composed = [] + inds_to_ignore = set() + outputs = set() + + for il, link in enumerate(links): + if il in inds_to_ignore: + continue + if links_composed: + if link[0] in outputs: + # Since `link` has as its input one of the outputs recorded from prior links in `links_composed` + # search through the links in `links_composed` to see which links need to be extended with `link` + # and the other links with the same input as `link` + link_extensions = [] + # Potential link extensions will only be located past the stage il that we are at + for il_e in range(il, len(links)): + if links[il_e][0] == link[0]: + link_extensions.append(links[il_e]) + outputs = outputs.union([links[il_e][-1]]) + inds_to_ignore = inds_to_ignore.union([il_e]) + + links_to_add = [] + inds_to_omit = [] + for il_c, link_c in enumerate(links_composed): + if link_c[-1] == link[0]: + inds_to_omit.append(il_c) + links_to_add.extend([link_c + [ext[-1]] for ext in link_extensions]) + + links_composed = [link_c for i, link_c in enumerate(links_composed) if i not in inds_to_omit] + links_to_add + else: + links_composed.append(link) + outputs = outputs.union([link[-1]]) + else: + links_composed.append(link) + outputs = outputs.union([link[-1]]) + return links_composed + + def find_io_links(block, input_args, block_input_args): """ For a given `block`, see which output arguments the input argument `input_args` affects @@ -104,4 +186,3 @@ def find_io_links(block, input_args, block_input_args): for o in J.outputs: links[o] = list(J.nesteddict[o].keys()) return links - From 3dc5dc2165d34ee3be45c2525383acdf7aee05c3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 16:32:29 -0600 Subject: [PATCH 019/288] Implement print_unknowns_targets_links and fix some print formatting --- sequence_jacobian/devtools/analysis.py | 82 ++++++++++++++++---------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index 6a1a507..068a1ab 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -36,11 +36,60 @@ def print_block_links(self, block_name): i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) print(f" {i_var} -> {o_var}") + print("") # To break lines def print_var_links(self, var_name, calibration=None, ignore_helpers=True): - print(f" Links for {var_name}") + print(f" Links from {var_name}") print(" " + "-" * (len(f" Links for {var_name}"))) + links = self.find_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) + + for link_c in links: + print(" " + " -> ".join(link_c)) + print("") # To break lines + + def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): + print(f"Links between {unknowns} and {targets}") + print(" " + "-" * (len(f"Links between {unknowns} and {targets}"))) + unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), + coords=[unknowns, targets], + dims=["inputs", "outputs"]) + for u in unknowns: + links = self.find_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) + for link in links: + if link[0] == u and link[-1] in targets: + unknown_target_net.loc[u, link[-1]] = 1. + print(unknown_target_net) + print("") # To break lines + + # User-facing "analysis" methods + def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args): + """ + Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG + + Parameters + ---------- + inputs_to_be_recorded: `list(str)` + A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork + block_input_args: `dict` + A dict of variable/parameter names and values (typically from the steady state of the model) on which, + a block can perform a valid evaluation + """ + block_inds_sorted = graph.block_sort(self.blocks_as_list) + for input_var in inputs_to_be_recorded: + all_input_vars = set(input_var) + for ib in block_inds_sorted: + ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} + # This extra step is needed because some arguments required for calling .jac on + # HetBlock and SolvedBlock are not a part of .inputs + ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) + io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) + if io_links: + self._record_io_links(self.blocks_names[ib], io_links) + # Need to also track the paths of outputs which could be intermediate inputs further down the DAG + all_input_vars = all_input_vars.union(set(io_links.keys())) + + def find_var_links(self, var_name, calibration=None, ignore_helpers=True): # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those # `outputs` and instantiate the initial list of links link_inds = np.nonzero(self._subset_by_vars(var_name).data) @@ -78,36 +127,7 @@ def print_var_links(self, var_name, calibration=None, ignore_helpers=True): links[il].append(o_var) if o_var in required: intermediates = intermediates.union(o_var) - - for link_c in _compose_dyad_links(links): - print(" -> ".join(link_c)) - - # User-facing "analysis" methods - def record_input_variable_paths(self, inputs_to_be_recorded, block_input_args): - """ - Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG - - Parameters - ---------- - inputs_to_be_recorded: `list(str)` - A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork - block_input_args: `dict` - A dict of variable/parameter names and values (typically from the steady state of the model) on which, - a block can perform a valid evaluation - """ - block_inds_sorted = graph.block_sort(self.blocks_as_list) - for input_var in inputs_to_be_recorded: - all_input_vars = set(input_var) - for ib in block_inds_sorted: - ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} - # This extra step is needed because some arguments required for calling .jac on - # HetBlock and SolvedBlock are not a part of .inputs - ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) - io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) - if io_links: - self._record_io_links(self.blocks_names[ib], io_links) - # Need to also track the paths of outputs which could be intermediate inputs further down the DAG - all_input_vars = all_input_vars.union(set(io_links.keys())) + return _compose_dyad_links(links) # Analysis support methods def _subset_by_block(self, block_name): From 994ea5f9e351fec220bce65f4e1f1f7bffa92c37 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 16:40:19 -0600 Subject: [PATCH 020/288] Fix block_sort issues with HelperBlocks and error with using union on multi-character strings --- sequence_jacobian/devtools/analysis.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index 068a1ab..a0c818c 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -12,8 +12,9 @@ class BlockIONetwork: A 3-d axis-labeled DataArray (blocks x inputs x outputs), which allows for the analysis of the input-output structure of a DAG. """ - def __init__(self, blocks): - topsorted, inset, outset = graph.block_sort(blocks, return_io=True) + def __init__(self, blocks, calibration=None, ignore_helpers=True): + topsorted, inset, outset = graph.block_sort(blocks, return_io=True, calibration=calibration, + ignore_helpers=ignore_helpers) self.blocks = {b.name: b for b in blocks} self.blocks_names = list(self.blocks.keys()) self.blocks_as_list = list(self.blocks.values()) @@ -63,7 +64,8 @@ def print_unknowns_targets_links(self, unknowns, targets, calibration=None, igno print("") # To break lines # User-facing "analysis" methods - def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args): + def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, + calibration=None, ignore_helpers=True): """ Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG @@ -74,8 +76,12 @@ def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args): block_input_args: `dict` A dict of variable/parameter names and values (typically from the steady state of the model) on which, a block can perform a valid evaluation + calibration: `dict` or None + Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies + ignore_helpers: bool + Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies """ - block_inds_sorted = graph.block_sort(self.blocks_as_list) + block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, ignore_helpers=ignore_helpers) for input_var in inputs_to_be_recorded: all_input_vars = set(input_var) for ib in block_inds_sorted: @@ -116,7 +122,7 @@ def find_var_links(self, var_name, calibration=None, ignore_helpers=True): # before and if it is an intermediate input links.append([intm_i_var, o_var]) if o_var in required: - intermediates = intermediates.union(o_var) + intermediates = intermediates.union([o_var]) # Check if `var_name` enters into that block as an input, and if so add the link between it and the output # it directly affects, recording that output as an intermediate input if needed @@ -126,7 +132,7 @@ def find_var_links(self, var_name, calibration=None, ignore_helpers=True): o_var = str(self._subset_by_vars(var_name).coords["outputs"][link_inds[1][il]].data) links[il].append(o_var) if o_var in required: - intermediates = intermediates.union(o_var) + intermediates = intermediates.union([o_var]) return _compose_dyad_links(links) # Analysis support methods From 461ff9bdbb932fbe6cbbfa40bd0edf09d43251d6 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 16:44:49 -0600 Subject: [PATCH 021/288] Make print_var_link sort links before printing for easier reading --- sequence_jacobian/devtools/analysis.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index a0c818c..5194a7a 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -44,9 +44,14 @@ def print_var_links(self, var_name, calibration=None, ignore_helpers=True): print(" " + "-" * (len(f" Links for {var_name}"))) links = self.find_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) - - for link_c in links: - print(" " + " -> ".join(link_c)) + link_strs = [] + + # Create " -> " linked strings and sort them for nicer printing + for link in links: + link_strs.append(" " + " -> ".join(link)) + link_strs.sort() + for link_str in link_strs: + print(link_str) print("") # To break lines def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): From 99cac6c8f59d6129c832f2ad85bd3df59262f976 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 17:13:04 -0600 Subject: [PATCH 022/288] Implement query_var_link and make print formatting more uniform --- sequence_jacobian/devtools/analysis.py | 36 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index 5194a7a..de2bba1 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -32,20 +32,24 @@ def print_block_links(self, block_name): print(" " + "-" * (len(f" Links in {block_name}"))) link_inds = np.nonzero(self._subset_by_block(block_name)).data + links = [] for i in range(np.shape(link_inds)[0]): i_ind, o_ind = link_inds[:, i] i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) - print(f" {i_var} -> {o_var}") - print("") # To break lines + links.append([i_var, o_var]) + self._print_links(links) - def print_var_links(self, var_name, calibration=None, ignore_helpers=True): + def print_all_var_links(self, var_name, calibration=None, ignore_helpers=True): print(f" Links from {var_name}") print(" " + "-" * (len(f" Links for {var_name}"))) - links = self.find_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) - link_strs = [] + links = self.find_all_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) + self._print_links(links) + @staticmethod + def _print_links(links): + link_strs = [] # Create " -> " linked strings and sort them for nicer printing for link in links: link_strs.append(" " + " -> ".join(link)) @@ -55,19 +59,33 @@ def print_var_links(self, var_name, calibration=None, ignore_helpers=True): print("") # To break lines def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): - print(f"Links between {unknowns} and {targets}") - print(" " + "-" * (len(f"Links between {unknowns} and {targets}"))) + print(f" Links between {unknowns} and {targets}") + print(" " + "-" * (len(f" Links between {unknowns} and {targets}"))) unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), coords=[unknowns, targets], dims=["inputs", "outputs"]) for u in unknowns: - links = self.find_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) + links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) for link in links: if link[0] == u and link[-1] in targets: unknown_target_net.loc[u, link[-1]] = 1. print(unknown_target_net) print("") # To break lines + # TODO: Implement an enhancement to display the "closest" link if missing. + def query_var_link(self, input_var, output_var, calibration=None, ignore_helpers=True): + all_links = self.find_all_var_links(input_var, calibration=calibration, ignore_helpers=ignore_helpers) + link_paths = [] + for link in all_links: + if link[0] == input_var and link[-1] == output_var: + link_paths.append(link) + if link_paths: + print(f" Links between {input_var} and {output_var}") + print(" " + "-" * (len(f" Links between {input_var} and {output_var}"))) + self._print_links(link_paths) + else: + print(f"There are no links within the DAG connecting {input_var} to {output_var}") + # User-facing "analysis" methods def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, calibration=None, ignore_helpers=True): @@ -100,7 +118,7 @@ def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, # Need to also track the paths of outputs which could be intermediate inputs further down the DAG all_input_vars = all_input_vars.union(set(io_links.keys())) - def find_var_links(self, var_name, calibration=None, ignore_helpers=True): + def find_all_var_links(self, var_name, calibration=None, ignore_helpers=True): # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those # `outputs` and instantiate the initial list of links link_inds = np.nonzero(self._subset_by_vars(var_name).data) From 30aafddf4953d0ab1307e851d311c244883273e0 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 17:34:00 -0600 Subject: [PATCH 023/288] Make warning formatting uniform across sequence_jacobian package --- sequence_jacobian/__init__.py | 9 +++++++++ sequence_jacobian/visualization/draw_dag.py | 9 +-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 65ffb51..529a62f 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -20,3 +20,12 @@ from .utilities import interpolate from .utilities.discretize import agrid from .utilities.optimized_routines import setmin + +# Ensure warning uniformity across package +import warnings + +# Force warnings.warn() to omit the source code line in the message +formatwarning_orig = warnings.formatwarning +warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ + formatwarning_orig(message, category, filename, lineno, line='') + diff --git a/sequence_jacobian/visualization/draw_dag.py b/sequence_jacobian/visualization/draw_dag.py index 7b81e9f..9b75ca5 100644 --- a/sequence_jacobian/visualization/draw_dag.py +++ b/sequence_jacobian/visualization/draw_dag.py @@ -1,14 +1,7 @@ """Provides the functionality for basic DAG visualization""" import warnings - -# Force warnings.warn() to omit the source code line in the message -formatwarning_orig = warnings.formatwarning -warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ - formatwarning_orig(message, category, filename, lineno, line='') - -from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph,\ - find_outputs_that_are_intermediate_inputs +from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph # Implement DAG drawing functions as "soft" dependencies to not enforce the installation of graphviz, since From 90ab689e083978a571e42aa72c00ec5cab4cdbf4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 17:52:47 -0600 Subject: [PATCH 024/288] Break out find_unknowns_targets_links to be used in debugging tools --- sequence_jacobian/devtools/analysis.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py index de2bba1..77e90d8 100644 --- a/sequence_jacobian/devtools/analysis.py +++ b/sequence_jacobian/devtools/analysis.py @@ -61,14 +61,8 @@ def _print_links(links): def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): print(f" Links between {unknowns} and {targets}") print(" " + "-" * (len(f" Links between {unknowns} and {targets}"))) - unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), - coords=[unknowns, targets], - dims=["inputs", "outputs"]) - for u in unknowns: - links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) - for link in links: - if link[0] == u and link[-1] in targets: - unknown_target_net.loc[u, link[-1]] = 1. + unknown_target_net = self.find_unknowns_targets_links(unknowns, targets, calibration=calibration, + ignore_helpers=ignore_helpers) print(unknown_target_net) print("") # To break lines @@ -158,6 +152,17 @@ def find_all_var_links(self, var_name, calibration=None, ignore_helpers=True): intermediates = intermediates.union([o_var]) return _compose_dyad_links(links) + def find_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): + unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), + coords=[unknowns, targets], + dims=["inputs", "outputs"]) + for u in unknowns: + links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) + for link in links: + if link[0] == u and link[-1] in targets: + unknown_target_net.loc[u, link[-1]] = 1. + return unknown_target_net + # Analysis support methods def _subset_by_block(self, block_name): return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] From 66f4d60324c6bf994e969fd0c6f9b1609ae44ff2 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Feb 2021 18:14:13 -0600 Subject: [PATCH 025/288] Complete simple debugging suite --- sequence_jacobian/devtools/debug.py | 86 ++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/devtools/debug.py b/sequence_jacobian/devtools/debug.py index 41b010a..420a136 100644 --- a/sequence_jacobian/devtools/debug.py +++ b/sequence_jacobian/devtools/debug.py @@ -1,15 +1,36 @@ """Top-level tools to help users debug their sequence-jacobian code""" +import warnings +import numpy as np + +from . import analysis from ..utilities import graph -def ensure_computability(blocks, calibration, unknowns, verbose=False): - ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=verbose) - # TODO: Ensure that all of the unknowns map in some way to at least one target each +def ensure_computability(blocks, calibration=None, unknowns_ss=None, + exogenous=None, unknowns=None, targets=None, ss=None, + verbose=False, fragile=True, ignore_helpers=True): + # Check if `calibration` and `unknowns` jointly have all of the required variables needed to be able + # to calculate a steady state. If ss is provided, assume the user doesn't need to check this + if calibration and unknowns_ss and not ss: + ensure_all_inputs_accounted_for(blocks, calibration, unknowns_ss, verbose=verbose, fragile=fragile) + + # Check if unknowns and exogenous are not outputs of any blocks, and that targets are not inputs to any blocks + if exogenous and unknowns and targets: + ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous + unknowns, targets, + verbose=verbose, fragile=fragile, + ignore_helpers=ignore_helpers, + calibration=calibration) + + # Check if there are any "broken" links between unknowns and targets, i.e. if there are any unknowns that don't + # affect any targets, or if there are any targets that aren't affected by any unknowns + if unknowns and targets and ss: + ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile, + ignore_helpers=ignore_helpers, calibration=calibration) # To ensure that no input argument that is required for one of the blocks to evaluate is missing -def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False): +def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False, fragile=True): variables_accounted_for = set(unknowns.keys()).union(set(calibration.keys())) all_inputs = set().union(*[b.inputs for b in blocks]) required = graph.find_outputs_that_are_intermediate_inputs(blocks) @@ -17,14 +38,38 @@ def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False variables_unaccounted_for = non_computed_inputs.difference(variables_accounted_for) if variables_unaccounted_for: - raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" - f"parameters: {variables_unaccounted_for}") + if fragile: + raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" + f"parameters: {variables_unaccounted_for}") + else: + warnings.warn(f"\nThe following variables were not listed as unknowns or provided as fixed variables/" + f"parameters: {variables_unaccounted_for}") if verbose: print("This DAG accounts for all inputs variables.") -def ensure_unknowns_and_targets_valid(blocks, unknowns, targets, verbose=False): - pass +def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unknowns, targets, + verbose=False, fragile=True, + ignore_helpers=True, calibration=None): + cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks, ignore_helpers=ignore_helpers, + calibration=calibration) + invalid_xu = [] + invalid_targ = [] + for xu in exogenous_unknowns: + if xu not in cand_xu: + invalid_xu.append(xu) + for targ in targets: + if targ not in cand_targets: + invalid_targ.append(targ) + if invalid_xu or invalid_targ: + if fragile: + raise RuntimeError(f"The following exogenous/unknowns are invalid candidates: {invalid_xu}\n" + f"The following targets are invalid candidates: {invalid_targ}") + else: + warnings.warn(f"\nThe following exogenous/unknowns are invalid candidates: {invalid_xu}\n" + f"The following targets are invalid candidates: {invalid_targ}") + if verbose: + print("The provided exogenous/unknowns and targets are all valid candidates for this DAG.") def find_candidate_unknowns_and_targets(block_list, verbose=False, ignore_helpers=True, calibration=None): @@ -42,3 +87,28 @@ def find_candidate_unknowns_and_targets(block_list, verbose=False, ignore_helper f"Candidate targets: {cand_targets}") return cand_xu, cand_targets + + +def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True, + calibration=None, ignore_helpers=True): + io_net = analysis.BlockIONetwork(blocks) + io_net.record_input_variables_paths(unknowns, ss, calibration=calibration, ignore_helpers=ignore_helpers) + ut_net = io_net.find_unknowns_targets_links(unknowns, targets, calibration=calibration, + ignore_helpers=ignore_helpers) + broken_unknowns = [] + broken_targets = [] + for u in unknowns: + if not np.any(ut_net.loc[u, :]): + broken_unknowns.append(u) + for t in targets: + if not np.any(ut_net.loc[:, t]): + broken_targets.append(t) + if broken_unknowns or broken_targets: + if fragile: + raise RuntimeError(f"The following unknowns don't affect any targets: {broken_unknowns}\n" + f"The following targets aren't affected by any unknowns: {broken_targets}") + else: + warnings.warn(f"\nThe following unknowns don't affect any targets: {broken_unknowns}\n" + f"The following targets aren't affected by any unknowns: {broken_targets}") + if verbose: + print("This DAG does not contain any broken links between unknowns and targets.") From 85cd9e1ffbf8199b878e7378ba30f5e7617d576d Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 11 Feb 2021 09:16:04 -0600 Subject: [PATCH 026/288] Add xarray as a dependency in order to use the debugging tools --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 933f4b5..854e244 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ numpy scipy numba +xarray From 7abc4a411675deca368d24eab76ca814de4f340b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 11 Feb 2021 11:23:15 -0600 Subject: [PATCH 027/288] Remove all asymptotic/determinacy functionality from sequence-jacobian --- sequence_jacobian/__init__.py | 2 +- sequence_jacobian/asymptotic.py | 208 ----------------------- sequence_jacobian/blocks/het_block.py | 83 --------- sequence_jacobian/blocks/solved_block.py | 10 +- sequence_jacobian/determinacy.py | 111 ------------ sequence_jacobian/jacobian/classes.py | 14 -- sequence_jacobian/jacobian/drivers.py | 64 ++----- sequence_jacobian/jacobian/support.py | 39 ----- tests/base/test_determinacy.py | 27 --- 9 files changed, 14 insertions(+), 544 deletions(-) delete mode 100644 sequence_jacobian/asymptotic.py delete mode 100644 sequence_jacobian/determinacy.py delete mode 100644 tests/base/test_determinacy.py diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 529a62f..0173d2d 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -1,6 +1,6 @@ """Public-facing objects.""" -from . import asymptotic, determinacy, estimation, jacobian, nonlinear, utilities, devtools +from . import estimation, jacobian, nonlinear, utilities, devtools from .models import rbc, krusell_smith, hank, two_asset diff --git a/sequence_jacobian/asymptotic.py b/sequence_jacobian/asymptotic.py deleted file mode 100644 index 906add9..0000000 --- a/sequence_jacobian/asymptotic.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Classes and methods for calculating asymptotic Jacobians""" - -import numpy as np -from numpy.fft import rfft, rfftn, irfft, irfftn - -from . import determinacy -from .jacobian.support import pack_asymptotic_jacobians, unpack_asymptotic_jacobians - - -class AsymptoticTimeInvariant: - """Represents the asymptotic behavior of infinite matrix that is asymptotically time invariant, - given by vector v of -(tau-1), ... , 0, ..., tau-1 asymptotic column entries around main diagonal. - - Conveniently overloads matrix multiplication operator @, addition operator +, etc., so that we - can use the same code on these as for ordinary matrices: if A and B are of the ATI class, - then A @ B is also of the ATI class and gives the asymptotic columns around diagonal of the - product of matrices whose asymptotic columns are given respectively by A and B.""" - - # give higher priority than simple_block.SimpleSparse, which when mixed with ATI is converted - # to it using .asymptotic_time_invariant property and then handled by methods in this class - __array_priority__ = 2000 - - def __init__(self, v): - self.v = v - - # v should be -(tau-1), ... , 0, ..., tau-1 asymp column around main diagonal - self.tau = (len(v)+1) // 2 - assert self.tau*2 - 1 == len(v), f'{len(v)}' - - @property - def vfft(self): - """FFT of v padded on the right with 2*tau-1 0s, used for multiplication below""" - # we could cache this, but so fast it isn't really necessary - # TODO: maybe it should be cached after all now that we don't need other stuff? - return rfft(self.v, 4*self.tau-3) - - def changetau(self, tau): - """Return new with lower or higher tau, trimming or padding with zeros as needed""" - if tau == self.tau: - return self - elif tau < self.tau: - return AsymptoticTimeInvariant(self.v[self.tau - tau: tau + self.tau - 1]) - else: - v = np.zeros(2*tau-1) - v[tau - self.tau: tau + self.tau - 1] = self.v - return AsymptoticTimeInvariant(v) - - def __getitem__(self, i): - """Get convenient slice of v, properly centered so -2 maps to entry v_(-2), etc.""" - if isinstance(i, slice): - return self.v[slice(i.start+self.tau-1, i.stop+self.tau-1, i.step)] - else: - return self.v[i+self.tau-1] - - @property - def T(self): - """Transpose""" - return AsymptoticTimeInvariant(self.v[::-1]) - - def __pos__(self): - return self - - def __neg__(self): - return AsymptoticTimeInvariant(-self.v) - - def __matmul__(self, other): - """If the vectors v and w represent the asymptotic diagonals of ATI matrices, their - the product of the matrices is ATI, with asymptotic diagonals represented by vector x - that is *convolution* of v and w: - - x[i] = sum_(j=-infty)^infty v[j]*w[i-j] - - If v and w both have nonzero elements with indices -(tau-1),...,(tau-1), then x[i] - will be nonzero for indices -(2*tau-2),...,(2*tau-2). - - We could obtain this full vector x using, e.g., np.convolve(v, w). - - When tau is large it is more efficient, however, to use the FFT: - irfft(rfft(v, 4*tau-3), rfft(w, 4*tau-3), 4*tau-3) is identical to np.convolve(v, w). - - By convention, to prevent exploding dimensionality, we then return the middle - -(tau-1), ..., (tau-1) elements of the convolution, dropping the extra (tau-1) on each side. - """ - if isinstance(other, AsymptoticTimeInvariant): - # make sure the two arguments have equal tau by enlarging the smaller - newself = self - if other.tau < self.tau: - other = other.changetau(self.tau) - elif other.tau > self.tau: - newself = self.changetau(other.tau) - - # convolve using FFT, then drop first and last (tau-1) entries - return AsymptoticTimeInvariant(irfft(newself.vfft*other.vfft, 4*newself.tau-3)[newself.tau-1:-(newself.tau-1)]) - elif hasattr(other, 'asymptotic_time_invariant'): - # if one of the arguments can be converted to ATI (for now, just SimpleSparse) - # do so and then take product - return self @ other.asymptotic_time_invariant - else: - return NotImplemented - - def __rmatmul__(self, other): - return self @ other - - def __add__(self, other): - if isinstance(other, AsymptoticTimeInvariant): - # make sure the two arguments have equal tau (same as matmul) - newself = self - if other.tau < self.tau: - other = other.changetau(self.tau) - elif other.tau > self.tau: - newself = self.changetau(other.tau) - - # now just add the corresponding vectors v - return AsymptoticTimeInvariant(newself.v + other.v) - elif hasattr(other, 'asymptotic_time_invariant'): - # convert non-ATI argument to ATI if possible (same as matmul) - return self + other.asymptotic_time_invariant - else: - return NotImplemented - - def __radd__(self, other): - return self + other - - def __sub__(self, other): - return self + (-other) - - def __rsub__(self, other): - return -self + other - - def __mul__(self, a): - if not np.isscalar(a): - return NotImplemented - return AsymptoticTimeInvariant(a*self.v) - - def __rmul__(self, a): - return self * a - - def __repr__(self): - return f'AsymptoticTimeInvariant({self.v!r})' - - def __eq__(self, other): - return np.array_equal(self.v, other.v) if isinstance(other, AsymptoticTimeInvariant) else False - - -def invert_jacdict(jacdict, unknowns, targets, tau, test_invertible=False): - """Given a nested dict of ATI Jacobians that maps unknowns -> targets, e.g. an asymptotic - H_U matrix, get the inverse H_U^(-1) as a nested dict. - - This is implemented by inverting the FFT-based multiplication that was implemented above - for ATI, making use of the linearity of the FFT: - - We take the FFT of each ATI Jacobian, padded out to 4*tau-3 as above - (This is done by first packing all Jacobians into a single array A) - - Then, we take the FFT of the identity, centered aroun d2*tau-1 since - we intend it to be the result of a product - - We solve frequency-by-frequency, i.e. for each of 4*tau-3 omegas we solve a k*k - linear system to get A_rfft[omega,...]^(-1)*id_rfft[omega,...] - - We take the inverse FFT of the results, then take only the first 2*tau-1 elements - to get (approximate) inverse Jacobians with times -(tau-1),...,(tau-1), same as - original Jacobians - - We unpack these to get a nested dict of ATI Jacobians that inverts original 'jacdict' - - Parameters - ---------- - jacdict : dict of dict, ATI (or convertible to ATI) Jacobians where jacdict[t][u] gives - asymptotic mapping from unknowns u to targets t in H_U - unknowns : list, names of unknowns in H_U - targets : list, names of targets in H_U - tau : int, convert all ATI Jacobians to size tau and provide inverse in size tau - test_invertible : [optional] bool, use winding number criterion to test whether we should - really be inverting this system (i.e. whether determinate solution) - - Returns - ------- - inv_jacdict : dict of dict, ATI Jacobians where inv_jacdict[u][t] gives asymptotic mapping - from targets t to unknowns u in H_U^(-1) - """ - - k = len(unknowns) - assert k == len(targets) - - # stack the k^2 Jacobians relating unknowns to targets into an A matrix - A = pack_asymptotic_jacobians(jacdict, unknowns, targets, tau) - - if test_invertible: - # use winding number criterion to test invertibility - if determinacy.winding_criterion(A, N=4096) != 0: - raise ValueError('Trying to invert asymptotic time invariant system of Jacobians' + - ' but winding number test says that it is not uniquely invertible!') - - # take FFT of first dimension (time) of A (i.e. take FFT separtely of all k^2 Jacobians) - A_rfft = rfftn(A, s=(4*tau-3,), axes=(0,)) - - # take FFT of identity operator (for efficiency, reuse smaller calc) - id_vec_rfft = rfft(np.arange(4*tau-3)==(2*tau-2)) - id_rfft = np.zeros((2*tau-1, k, k), dtype=np.complex128) - for i in range(k): - id_rfft[:, i, i] = id_vec_rfft - - # now solve the linear system to invert A frequency-by-frequency - # (since frequency is leading dimension, np.linalg.solve automatically does this) - A_rfft_inv = np.linalg.solve(A_rfft, id_rfft) - - # take inverse FFT of this to get full A - # then take first 2*tau-1 entries to get approximate A from -(tau-1),...,0,...,(tau-1) - A_inv = irfftn(A_rfft_inv, s=(4*tau-3,), axes=(0,))[:2*tau-1, :, :] - - # unstack this - return unpack_asymptotic_jacobians(A_inv, targets, unknowns, tau) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index ea78902..0bb6846 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -2,7 +2,6 @@ import copy from .. import utilities as utils -from .. import asymptotic from ..jacobian.classes import JacobianDict @@ -417,88 +416,6 @@ def jac(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved return JacobianDict(J) - def ajac(self, ss, T, shock_list, output_list=None, h=1E-4, Tpost=None, save=False, use_saved=False): - """Like .jac, but outputs asymptotic columns of Jacobians as AsymptoticTimeInvariant objects - with nonzero entries -(T-1),...,(Tpost-1) representing asymptotic entries in diagonals, - measured relative to main diagonal. - - Does additional iteration on curlyPs as necessary to extend Tpost beyond T, since common case - is that curlyYs and curlyDs from backward iteration converge to zero much more quickly than - curlyPs from forward iteration.""" - - # default outputs are just all outputs of back it function except backward variables - if output_list is None: - output_list = self.non_back_iter_outputs - - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in shock_list] - - # if Tpost not provided, assume it is 2*T by default - if Tpost is None: - Tpost = 2*T - elif Tpost < T: - raise ValueError(f'must have Tpost={Tpost} less than T={T}') - - # saved last by ajac, directly extract - if use_saved and 'curlyYs' not in self.saved: - asympJ = {} - for o in output_list: - asympJ[o.capitalize()] = {} - for i in relevant_shocks: - asympJ[o.capitalize()][i] = asymptotic.AsymptoticTimeInvariant( - self.saved['asympJ'][o.capitalize()][i][-(Tpost-1): Tpost]) - return asympJ - - # was either saved last by jac or not saved at all, need to do more work! - - # step 0: preliminary processing of steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, - sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss, save, use_saved) - - if use_saved and 'curlyYs' in self.saved: - # was saved by jac, first copy curlyYs, curlyDs, curlyPs - curlyYs = utils.misc.extract_nested_dict(savedA=self.saved['curlyYs'], - keys1=relevant_shocks, keys2=output_list, shape=(T,)) - curlyDs = utils.misc.extract_dict(savedA=self.saved['curlyDs'], keys=relevant_shocks, shape=(T,)) - curlyPs_old = utils.misc.extract_dict(savedA=self.saved['curlyPs'], keys=output_list, shape=(T - 1,)) - - # now need curlyPs that go to T+Tpost-1, not just T - curlyPs = {} - for o in output_list: - curlyP_extrarows = self.forward_iteration_fakenews(curlyPs_old[o][-1, ...], - Pi, sspol_i, sspol_pi, Tpost) - curlyPs[o] = np.concatenate((curlyPs_old[o][:-1, ...], curlyP_extrarows), axis=0) - else: - # was not saved at all, get curlyYs, curlyDs, curlyPs for ourselves - # step 1: compute curlyY and curlyD (backward iteration) for each input i (same as jac) - curlyYs, curlyDs = {}, {} - for i in relevant_shocks: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, output_list, - ssin_dict, ssout_list, ss['D'], Pi.T.copy(), sspol_i, - sspol_pi, sspol_space, T, h, ss_for_hetinput) - - # step 2: compute prediction vectors curlyP (forward iteration) for each outcome o - # here go to (T-1) + (Tpost-1) rather than (T-1) - curlyPs = {} - for o in output_list: - curlyPs[o] = self.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1+Tpost-1) - - # steps 3-4: make fake news matrix and Jacobian for each outcome-input pair - J = {o.capitalize(): {} for o in output_list} - asympJ = {o.capitalize(): {} for o in output_list} - for o in output_list: - for i in relevant_shocks: - F = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) - J[o.capitalize()][i] = HetBlock.J_from_F(F) - asympJ[o.capitalize()][i] = asymptotic.AsymptoticTimeInvariant( - np.concatenate((np.zeros(Tpost-T), J[o.capitalize()][i][:, -1]))) - - # if supposed to save, record J and asympJ for use by jac or ajac - if save: - self.saved_shock_list, self.saved_output_list = relevant_shocks, output_list - self.saved = {'J': J, 'asympJ': asympJ} - - return asympJ - def add_hetinput(self, hetinput, overwrite=False, verbose=True): """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process inputs through the hetinput function. diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index b4f28a6..6b19621 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,5 +1,5 @@ from .. import nonlinear -from ..jacobian.drivers import get_G, get_G_asymptotic +from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict from ..steady_state import steady_state from ..blocks.simple_block import simple @@ -72,11 +72,3 @@ def jac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False): # H_U_factored caching could be helpful here too return get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, T, ss, output_list, save=save, use_saved=use_saved) - - def ajac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False, Tpost=None): - relevant_shocks = [i for i in self.inputs if i in shock_list] - - if Tpost is None: - Tpost = 2*T - return get_G_asymptotic(self.block_list, relevant_shocks, list(self.unknowns.keys()), - self.targets, T, ss, output_list, save=save, use_saved=use_saved, Tpost=Tpost) diff --git a/sequence_jacobian/determinacy.py b/sequence_jacobian/determinacy.py deleted file mode 100644 index 39a8f8b..0000000 --- a/sequence_jacobian/determinacy.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Functions for implementing the winding criterion method for assessing model determinacy""" - -import numpy as np -from numpy.fft import rfftn -from numba import njit - - -def winding_criterion(A, N=4096): - """Build path of det A(lambda) and obtain its winding number, implementing winding number - criterion for determinacy that generalizes Onatski (2006). - - Parameters - ---------- - A : array ((2T-1)*k*k) - asymptotic H_U matrix, where A[t,i,j] gives Jacobian of target i vs. unknown j - at t-(T-1) above the main diagonal - N : [optional] int - number of equispaced points lambda on interval [0,2pi] for evaluating det A(lambda) - - Returns - ---------- - winding_number : int - winding number that characterizes existence and uniqueness of solutions: - 0 for determinate solution - -1 (or lower) for indeterminacy - 1 (or higher) for no solution - """ - det_Alambda = detA_path(A, N) - return winding_number(det_Alambda.real, det_Alambda.imag) - - -def detA_path(A, N=4096): - """Evaluates det A(lambda) at N equispaced points lambda on interval [0,2pi]. - - A brief derivation of how this function uses FFT to rapidly evaluate det A(lambda) follows. - - We have, letting A_(-j) denote the k*k matrix A[-j,:,:]: - - det A(lambda) = det sum_(j=-(T-1))^(T-1) A_(-j)e^(i*j*lambda) - - which, flipping the order and realigning j, can be rewritten as - - e^(lambda*i*k*(T-1)) det sum_(j=0)^(2T-2) A_(-j+(T-1))e^(-i*j*lambda) (***) - - Taking the sum in (***) for the values lambda=0,2*pi/N,...,2*pi*(N-1)/N, assuming N >= (2T-1), - is just taking the discrete Fourier transform of the sequence A_(T-1),...,A_(-(T-1)),0,...,0 - right-padded with zeros to length N. - - Hence we can rapidly, simultaneously evaluate (***) at all points lambda equispaced from lambda=0 - to lambda=2*pi using the FFT. This is implemented below, with additional efficiency from fact that - A(lambda) and A(2*pi-lambda) are conjugate. - """ - # preliminary: assume and verify shape 2*T-1, k, k for A - T = (A.shape[0]+1) // 2 - k = A.shape[1] - if not (T == (A.shape[0]+1)/2 and N >= 2*T-1 and k == A.shape[2]): - raise ValueError(f'Asymptotic A matrix has improper shape {A.shape}') - - # step 1: use FFT to calculate A(lambda) for each lambda = 2*pi*{0, 1/N, ..., 1/2} (last if N even) - # note that we need to reverse order of A_t to get sequence A_(T-1),...,A_(-(T-1)),0,...,0 - Alambda = rfftn(A[::-1,...], axes=(0,), s=(N,)) - - # step 2: take determinant of each, then multiply by e^(i*k*(T-1)*lambda) to get (***) - det_Alambda = np.empty(N+1, dtype=np.complex128) - det_Alambda[:N//2+1] = np.linalg.det(Alambda)*np.exp(2j*np.pi*k*(T-1)/N*np.arange(N//2+1)) - - # step 3: use conjugate symmetry to fill in rest - det_Alambda[N//2+1:] = det_Alambda[:(N+1)//2][::-1].conj() - - return det_Alambda - - -@njit -def winding_number(x, y): - """Compute winding number around origin of (x,y) coordinates that make closed path by - counting number of counterclockwise crossings of ray from (0,0) -> (infty,0) on x axis""" - # ensure closed path! - assert x[-1] == x[0] and y[-1] == y[0] - - winding_number = 0 - - # we iterate through coordinates (x[i], y[i]), where cur_sign is flag for - # whether current coordinate is above the x axis - cur_sign = (y[0] >= 0) - for i in range(1, len(x)): - if (y[i] >= 0) != cur_sign: - # if we're here, this means the x axis has been crossed - # this generally happens rarely, so efficiency no biggie - cur_sign = (y[i] >= 0) - - # crossing of x axis implies possible crossing of ray (0,0) -> (infty,0) - # we will evaluate three possible cases to see if this is indeed the case - if x[i] > 0 and x[i-1] > 0: - # case 1: both (x[i-1],y[i-1]) and (x[i],y[i]) on right half-plane, definite crossing - # increment winding number if counterclockwise (negative to positive y) - # decrement winding number if clockwise (positive to negative y) - winding_number += 2*cur_sign-1 - elif not (x[i] <= 0 and x[i-1] <= 0): - # here we've ruled out case 2: both (x[i-1],y[i-1]) and (x[i],y[i]) in left - # half-plane, where there is definitely no crossing - - # thus we're in ambiguous case 3, where points (x[i-1],y[i-1]) and (x[i],y[i]) in - # different half-planes: here we must analytically check whether we crossed - # x-axis to the right or the left of the origin - # [this step is intended to be rare] - cross_coord = (x[i-1]*y[i] - x[i]*y[i-1])/(y[i]-y[i-1]) - if cross_coord > 0: - winding_number += 2*cur_sign-1 - return winding_number - - diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index b42cc9d..aaf7694 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -4,7 +4,6 @@ import numpy as np from . import support -from .. import asymptotic class IdentityMatrix: @@ -102,10 +101,6 @@ def __pos__(self): def __repr__(self): return 'ZeroMatrix' - @property - def asymptotic_time_invariant(self): - return self.sparse().asymptotic_time_invariant - class SimpleSparse: """Efficient representation of sparse linear operators, which are linear combinations of basis @@ -162,15 +157,6 @@ def array(self): self.indices, self.xs = np.array(indices), np.array(xs) return self.indices, self.xs - @property - def asymptotic_time_invariant(self): - indices, xs = self.array() - tau = np.max(np.abs(indices[:, 0]))+1 # how far out do we go? - v = np.zeros(2*tau-1) - #v[indices[:, 0]+tau-1] = xs - v[-indices[:, 0]+tau-1] = xs # switch from asymptotic ROW to asymptotic COLUMN - return asymptotic.AsymptoticTimeInvariant(v) - @property def T(self): """Transpose""" diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 7c631a4..0d10e23 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -3,22 +3,21 @@ import numpy as np from .classes import JacobianDict -from .support import pack_vectors, unpack_vectors, pack_asymptotic_jacobians -from .. import asymptotic +from .support import pack_vectors, unpack_vectors from ..utilities import misc, graph +from ..blocks.simple_block import SimpleBlock '''Drivers: - get_H_U : get H_U matrix mapping all unknowns to all targets - get_impulse : get single GE impulse response - get_G : get G matrices characterizing all GE impulse responses - - get_G_asymptotic : get asymptotic diagonals of the G matrices returned by get_G - curlyJs_sorted : get block Jacobians curlyJ and return them topologically sorted - forward_accumulate : forward accumulation on DAG, taking in topologically sorted Jacobians ''' -def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=None, save=False, use_saved=False): +def get_H_U(block_list, unknowns, targets, T, ss=None, save=False, use_saved=False): """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. Parameters @@ -29,8 +28,6 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=N T : int, truncation horizon (if asymptotic, truncation horizon for backward iteration in HetBlocks) ss : [optional] dict, steady state required if block_list contains any non-jacdicts - asymptotic : [optional] bool, flag for returning asymptotic H_U - Tpost : [optional] int, truncation horizon for asymptotic -(Tpost-1),...,0,...,(Tpost-1) save : [optional] bool, flag for saving Jacobians inside HetBlocks use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks @@ -44,20 +41,13 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, asymptotic=False, Tpost=N """ # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(block_list, unknowns, ss, T, asymptotic, Tpost, save, use_saved) + curlyJs, required = curlyJ_sorted(block_list, unknowns, ss, T, save, use_saved) # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) - if not asymptotic: - # pack these n_u^2 matrices, each T*T, into a single matrix - return H_U_unpacked[targets, unknowns].pack(T) - # return pack_jacobians(H_U_unpacked, unknowns, targets, T) - else: - # pack these n_u^2 AsymptoticTimeInvariant objects into a single (2*Tpost-1,n_u,n_u) array - if Tpost is None: - Tpost = 2 * T - return pack_asymptotic_jacobians(H_U_unpacked, unknowns, targets, Tpost) + # pack these n_u^2 matrices, each T*T, into a single matrix + return H_U_unpacked[targets, unknowns].pack(T) def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None, @@ -187,32 +177,7 @@ def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def get_G_asymptotic(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, - save=False, use_saved=False, Tpost=None): - """Like get_G, but rather than returning the actual matrices G, return - asymptotic.AsymptoticTimeInvariant objects representing their asymptotic columns.""" - - # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(block_list, unknowns + exogenous, ss, T, save=save, - use_saved=use_saved, asymptotic=True, Tpost=Tpost) - - # step 2: do (matrix) forward accumulation to get - # H_U = J^(curlyH, curlyU) - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) - - # step 3: invert H_U and forward accumulate to get G_U = H_U^(-1)H_Z - U_H_unpacked = asymptotic.invert_jacdict(J_curlyH_U, unknowns, targets, Tpost) - G_U = forward_accumulate(curlyJs + [U_H_unpacked], exogenous, unknowns, required | set(targets)) - - # step 4: forward accumulation to get all outputs starting with G_U - # by default, don't calculate targets! - curlyJs = [G_U] + curlyJs - if outputs is None: - outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) - - -def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=None, save=False, use_saved=False): +def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=False): """ Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs and with respect to outputs of other blocks @@ -223,8 +188,6 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=N inputs : list, input names we need to differentiate with respect to ss : [optional] dict, steady state, needed if block_list includes blocks themselves T : [optional] int, horizon for differentiation, needed if block_list includes hetblock itself - asymptotic : [optional] bool, flag for returning asymptotic Jacobians - Tpost : [optional] int, truncation horizon for asymptotic -(Tpost-1),...,0,...,(Tpost-1) save : [optional] bool, flag for saving Jacobians inside HetBlocks use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks @@ -249,15 +212,12 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, asymptotic=False, Tpost=N shocks = set(inputs) | required for num in topsorted: block = block_list[num] - if hasattr(block, 'ajac'): - # has 'ajac' function, is some block other than SimpleBlock - if asymptotic: - jac = block.ajac(ss, T=T, shock_list=list(shocks), Tpost=Tpost, save=save, use_saved=use_saved) + + if hasattr(block, 'jac'): + if isinstance(block, SimpleBlock): + jac = block.jac(ss, shock_list=list(shocks)) else: - jac = block.jac(ss, T=T, shock_list=list(shocks), save=save, use_saved=use_saved) - elif hasattr(block, 'jac'): - # has 'jac' but not 'ajac', must be SimpleBlock where no distinction (given SimpleSparse) - jac = block.jac(ss, shock_list=list(shocks)) + jac = block.jac(ss, shock_list=list(shocks), T=T, save=save, use_saved=use_saved) else: # doesn't have 'jac', must be nested dict that is jac directly jac = block diff --git a/sequence_jacobian/jacobian/support.py b/sequence_jacobian/jacobian/support.py index d908a31..8ab3b76 100644 --- a/sequence_jacobian/jacobian/support.py +++ b/sequence_jacobian/jacobian/support.py @@ -3,8 +3,6 @@ import numpy as np from numba import njit -from .. import asymptotic - # For supporting SimpleSparse def multiply_basis(t1, t2): @@ -78,34 +76,6 @@ def multiply_rs_matrix(indices, xs, A): return Aout -def pack_asymptotic_jacobians(jacdict, inputs, outputs, tau): - """If we have -(tau-1),...,(tau-1) AsymptoticTimeInvariant Jacobians (or SimpleSparse) from - nI inputs to nO outputs in jacdict, combine into (2*tau-1,nO,nI) array A""" - nI, nO = len(inputs), len(outputs) - A = np.empty((2*tau-1, nI, nO)) - for iO in range(nO): - subdict = jacdict.get(outputs[iO], {}) - for iI in range(nI): - if inputs[iI] in subdict: - A[:, iO, iI] = make_ATI_v(jacdict[outputs[iO]][inputs[iI]], tau) - else: - A[:, iO, iI] = 0 - return A - - -def unpack_asymptotic_jacobians(A, inputs, outputs, tau): - """If we have (2*tau-1, nO, nI) array A where each A[:,o,i] is vector for AsymptoticTimeInvariant - Jacobian mapping output o to output i, output nested dict of AsymptoticTimeInvariant objects""" - nI, nO = len(inputs), len(outputs) - - jacdict = {} - for iO in range(nO): - jacdict[outputs[iO]] = {} - for iI in range(nI): - jacdict[outputs[iO]][inputs[iI]] = asymptotic.AsymptoticTimeInvariant(A[:, iO, iI]) - return jacdict - - def pack_vectors(vs, names, T): v = np.zeros(len(names)*T) for i, name in enumerate(names): @@ -128,12 +98,3 @@ def make_matrix(A, T): return A.matrix(T) else: return A - - -def make_ATI_v(x, tau): - """If x is either a AsymptoticTimeInvariant or something that can be converted to it, e.g. - SimpleSparse, report the underlying length 2*tau-1 vector with entries -(tau-1),...,(tau-1)""" - if not isinstance(x, asymptotic.AsymptoticTimeInvariant): - return x.asymptotic_time_invariant.changetau(tau).v - else: - return x.v diff --git a/tests/base/test_determinacy.py b/tests/base/test_determinacy.py deleted file mode 100644 index dd85415..0000000 --- a/tests/base/test_determinacy.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Test all models' determinacy calculations""" -from sequence_jacobian import determinacy, get_H_U - - -def test_hank_determinacy(one_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = one_asset_hank_model - T = 100 - - # Stable Case - A = get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True, save=True) - wn = determinacy.winding_criterion(A) - assert wn == 0 - - # Unstable Case - ss_unstable = {**ss, "phi": 0.75} - A_unstable = get_H_U(blocks, unknowns, targets, T, ss_unstable, asymptotic=True, use_saved=True) - wn_unstable = determinacy.winding_criterion(A_unstable) - assert wn_unstable == -1 - - -def test_two_asset_determinacy(two_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = two_asset_hank_model - T = 100 - - A = get_H_U(blocks, unknowns, targets, T, ss, asymptotic=True) - wn = determinacy.winding_criterion(A) - assert wn == 0 \ No newline at end of file From 8cc31c949344a0dcbbd25a20b7f5216ac6e1b968 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 11 Feb 2021 21:42:09 -0600 Subject: [PATCH 028/288] Implement provide_solver_default to try figure out a reasonable default for a given set of unknowns --- sequence_jacobian/steady_state.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state.py index 1598096..cbe5dc9 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state.py @@ -4,6 +4,7 @@ import scipy.optimize as opt from copy import deepcopy +from numbers import Real from . import utilities as utils from .utilities.misc import unprime, dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs from .blocks.simple_block import SimpleBlock @@ -116,6 +117,27 @@ def find_target_block(blocks, target): return block +def provide_solver_default(unknowns): + if len(unknowns) == 1: + bounds = list(unknowns.values())[0] + if not isinstance(bounds, tuple) or bounds[0] > bounds[1]: + raise ValueError("Unable to find a compatible one-dimensional solver with provided `unknowns`.\n" + " Please provide valid lower/upper bounds, e.g. unknowns = {`a`: (0, 1)}") + else: + return "brentq" + elif len(unknowns) > 1: + init_values = list(unknowns.values()) + if not np.all([isinstance(v, Real) for v in init_values]): + raise ValueError("Unable to find a compatible multi-dimensional solver with provided `unknowns`.\n" + " Please provide valid initial values, e.g. unknowns = {`a`: 1, `b`: 2}") + else: + return "broyden_custom" + else: + raise ValueError("`unknowns` is empty! Please provide a dict of keys/values equal to the number of unknowns" + " that need to be solved for.") + + + # Allow targets to be specified in the following formats # 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) # 2) target = {"r": 0.01} (allowing for the target to be non-zero) From fd3288d6a42e0b814955f5c13d03325793ee2610 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 12 Feb 2021 10:39:23 -0600 Subject: [PATCH 029/288] Implement __bool__ method on JacobianDicts to evaluate to true iff .outputs and .inputs are non-empty --- sequence_jacobian/jacobian/classes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index aaf7694..270e327 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -366,6 +366,9 @@ def __matmul__(self, x): else: return self.apply(x) + def __bool__(self): + return bool(self.outputs) and bool(self.inputs) + def compose(self, J): o_list = self.outputs m_list = tuple(set(self.inputs) & set(J.outputs)) From f094d6644d509ce1c5ff1b67a7ebd275b862b7b9 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Feb 2021 16:54:18 -0600 Subject: [PATCH 030/288] Flatten higher dimensional variables when comparing steady state values, so the infinity norm works --- sequence_jacobian/devtools/upgrade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/devtools/upgrade.py b/sequence_jacobian/devtools/upgrade.py index a547195..e2994d8 100644 --- a/sequence_jacobian/devtools/upgrade.py +++ b/sequence_jacobian/devtools/upgrade.py @@ -27,7 +27,7 @@ def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): if np.isscalar(ss_ref[key_ref]): print(f"{key_ref} resid: {abs(ss_ref[key_ref] - ss_comp[key_comp])}") else: - print(f"{key_ref} resid: {np.linalg.norm(ss_ref[key_ref] - ss_comp[key_comp], np.inf)}") + print(f"{key_ref} resid: {np.linalg.norm(ss_ref[key_ref].flatten() - ss_comp[key_comp].flatten(), np.inf)}") else: assert np.isclose(ss_ref[key_ref], ss_comp[key_comp]) From b7fceca339245a8fdcf353e1eff3b50792545140 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Feb 2021 17:08:48 -0600 Subject: [PATCH 031/288] Add .complete() to the J_om and J_mi intermediary matrices in the .compose method to ensure it's always valid to compose O/w you may have J_on and J_mi where n \neq m, and the code will fail --- sequence_jacobian/jacobian/classes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 270e327..e6257dd 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -332,6 +332,8 @@ def update(self, J): self.outputs = self.outputs + J.outputs self.nesteddict = {**self.nesteddict, **J.nesteddict} + # Ensure that every output in self has either a Jacobian or filler value for each input, + # s.t. all inputs map to all outputs def complete(self, filler): nesteddict = {} for o in self.outputs: @@ -374,8 +376,8 @@ def compose(self, J): m_list = tuple(set(self.inputs) & set(J.outputs)) i_list = J.inputs - J_om = self.nesteddict - J_mi = J.nesteddict + J_om = self.complete().nesteddict + J_mi = J.complete().nesteddict J_oi = {} for o in o_list: From 29009db76488ce3e0e4b11c3ea0ce3dda7171899 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Feb 2021 17:09:44 -0600 Subject: [PATCH 032/288] Implement CombinedBlock and deprecate .ss/.td/.jac in favor of .steady_state/.impulse_nonlinear/.impulse_linear/.jacobian in all other Blocks --- sequence_jacobian/__init__.py | 2 + sequence_jacobian/base.py | 11 +- sequence_jacobian/blocks/combined_block.py | 124 ++++++++++++++++++--- sequence_jacobian/blocks/helper_block.py | 9 +- sequence_jacobian/blocks/het_block.py | 52 +++++++-- sequence_jacobian/blocks/simple_block.py | 45 ++++++-- sequence_jacobian/blocks/solved_block.py | 37 ++++-- 7 files changed, 233 insertions(+), 47 deletions(-) diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 0173d2d..3e70626 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -4,6 +4,8 @@ from .models import rbc, krusell_smith, hank, two_asset +from .base import create_model + from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput from .blocks.helper_block import helper diff --git a/sequence_jacobian/base.py b/sequence_jacobian/base.py index df40714..ed6796f 100644 --- a/sequence_jacobian/base.py +++ b/sequence_jacobian/base.py @@ -1,10 +1,13 @@ """Type aliases, custom functions and errors for the base-level functionality of the package""" -from typing import Any - from .primitives import Block +from .blocks.combined_block import CombinedBlock, combine # Useful type aliases -Array = Any -BlockArray = Array[Block] +Model = CombinedBlock + + +# Useful functional aliases +def create_model(*args, **kwargs): + return combine(*args, model_alias=True, **kwargs) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 307a380..b918f54 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -1,20 +1,33 @@ """CombinedBlock class and the combine function to generate it""" from copy import deepcopy +import numpy as np from .. import utilities as utils -from ..steady_state import eval_block_ss +from ..steady_state import eval_block_ss, steady_state, provide_solver_default +from ..nonlinear import td_solve +from ..jacobian.drivers import get_G, forward_accumulate, curlyJ_sorted +from ..jacobian.classes import JacobianDict -def combine(*args): +def combine(*args, name="", model_alias=False): # TODO: Implement a check that all args are child types of AbstractBlock, when that is properly implemented - return CombinedBlock(*args) + return CombinedBlock(*args, name=name, model_alias=model_alias) class CombinedBlock: + # To users: Do *not* manually change the attributes via assignment. Instantiating a + # CombinedBlock has some automated features that are inferred from initial instantiation but not from + # re-assignment of attributes post-instantiation. + def __init__(self, *blocks, name="", model_alias=False): + self._blocks_unsorted = blocks + + self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated + self.blocks_w_helpers = None + + self._sorted_indices_w_o_helpers = utils.graph.block_sort(blocks, ignore_helpers=True) + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] - def __init__(self, *args, name=""): - self.blocks = args if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" else: @@ -27,15 +40,98 @@ def __init__(self, *args, name=""): all_inputs = set().union(*[block.inputs for block in self.blocks]) self.inputs = all_inputs.difference(self.outputs) - self.blocks_sorted_indices = utils.graph.block_sort(self.blocks) + self._model_alias = model_alias + + def __repr__(self): + if self._model_alias: + return f"" + else: + return f"" + + def steady_state(self, **calibration): + # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices + # accounting for HelperBlocks + if self._sorted_indices_w_helpers is None: + self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + calibration=calibration) + self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] - def ss(self, **kwargs): - ss_values = deepcopy(kwargs) - for i in self.blocks_sorted_indices: - ss_values.update(eval_block_ss(self.blocks[i], ss_values)) - return ss_values + ss_partial_eq = deepcopy(calibration) + for block in self.blocks_w_helpers: + ss_partial_eq.update(eval_block_ss(block, ss_partial_eq)) + return ss_partial_eq + + def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True): + irf_nonlin_partial_eq = {k: ss[k] + v for k, v in shocked_paths.items()} + for block in self.blocks: + input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} + # To only return dynamic paths of variables that do not remain at their steady state value + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, **input_args).items() + if not np.all(v == ss[k])}) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_partial_eq.items()} + else: + return irf_nonlin_partial_eq + + def impulse_linear(self, ss, shocked_paths, T=None, in_deviations=True): + shocked_vars = list(shocked_paths.keys()) + if T is None: + T = len(list(shocked_paths.values())[0]) + J_partial_eq = self.jacobian(ss, T=T, shocked_vars=shocked_vars) + irf_lin_partial_eq = J_partial_eq.apply(shocked_paths) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_partial_eq.items()} + else: + return irf_lin_partial_eq + + def jacobian(self, ss, T=None, shocked_vars=None, outputs=None, save=False, use_saved=False): + if shocked_vars is None: + return JacobianDict({}) + else: + curlyJs, required = curlyJ_sorted(self.blocks, shocked_vars, ss, T=T, save=save, use_saved=use_saved) + J_partial_eq = forward_accumulate(curlyJs, shocked_vars, outputs=outputs, required=required) + return J_partial_eq + + def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwargs): + # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices + # accounting for HelperBlocks + if self._sorted_indices_w_helpers is None: + self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + calibration=calibration) + self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] + + if solver is None: + solver = provide_solver_default(unknowns) + ss_gen_eq = steady_state(self.blocks_w_helpers, calibration, unknowns, targets, solver=solver, **kwargs) + return ss_gen_eq + + def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): + irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, + **{k: ss[k] + v for k, v in exogenous.items()}, **kwargs) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_gen_eq.items()} + else: + return irf_nonlin_gen_eq + + def solve_impulse_linear(self, ss, exogenous, unknowns, targets, T=None, in_deviations=True, **kwargs): + if T is None: + T = len(list(exogenous.values())[0]) + J_gen_eq = get_G(self.blocks, list(exogenous.keys()), unknowns, targets, T=T, ss=ss, **kwargs) + irf_lin_gen_eq = J_gen_eq.apply(exogenous) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_gen_eq.items()} + else: + return irf_lin_gen_eq - # TODO: Define td method for CombinedBlock - def td(self, ss, **kwargs): - pass + def solve_jacobian(self, ss, exogenous, unknowns, targets, T=None, **kwargs): + J_gen_eq = get_G(self.blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) + return J_gen_eq diff --git a/sequence_jacobian/blocks/helper_block.py b/sequence_jacobian/blocks/helper_block.py index 6250860..066c0c4 100644 --- a/sequence_jacobian/blocks/helper_block.py +++ b/sequence_jacobian/blocks/helper_block.py @@ -1,5 +1,7 @@ """HelperBlock class and @helper decorator to generate it""" +import warnings + from ..utilities import misc @@ -23,6 +25,11 @@ def __init__(self, f): def __repr__(self): return f"" + # TODO: Deprecated methods, to be removed! + def ss(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(*args, **kwargs) + def _output_in_ss_format(self, *args, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" @@ -33,7 +40,7 @@ def _output_in_ss_format(self, *args, **kwargs): # Currently does not use any of the machinery in SimpleBlock to deal with time displacements and hence # can handle non-scalar inputs. - def ss(self, *args, **kwargs): + def steady_state(self, *args, **kwargs): args = [x for x in args] kwargs = {k: v for k, v in kwargs.items()} return self._output_in_ss_format(*args, **kwargs) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 0bb6846..fc2d1af 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -1,5 +1,6 @@ -import numpy as np +import warnings import copy +import numpy as np from .. import utilities as utils from ..jacobian.classes import JacobianDict @@ -159,8 +160,26 @@ def __repr__(self): each backward iteration step in td ''' - def ss(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000, - hetoutput=False, **kwargs): + # TODO: Deprecated methods, to be removed! + def ss(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(*args, **kwargs) + + def td(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(*args, **kwargs) + + def jac(self, ss, T=None, shock_list=None, **kwargs): + if shock_list is None: + shock_list = [] + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, T, shock_list, **kwargs) + + def steady_state(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000, + hetoutput=False, **kwargs): """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. Parameters @@ -230,7 +249,7 @@ def ss(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_ return ss - def td(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **kwargs): + def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **shocked_vars): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in kwargs are constant at their ss values. Analog to SimpleBlock.td. @@ -248,7 +267,7 @@ def td(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **kwa return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies - kwargs : dict of {str : array(T, ...)} + shocked_vars : dict of {str : array(T, ...)} all time-varying inputs here, with first dimension being time this must have same length T for all entries (all outputs will be calculated up to T) @@ -261,10 +280,10 @@ def td(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **kwa of self.back_Step_fun on the full grid """ - # infer T from kwargs, check that all shocks have same length - shock_lengths = [x.shape[0] for x in kwargs.values()] + # infer T from shocked_vars, check that all shocks have same length + shock_lengths = [x.shape[0] for x in shocked_vars.values()] if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs are same length!') + raise ValueError('Not all shocks in kwargs (shocked_vars) are same length!') T = shock_lengths[0] # copy from ss info @@ -290,8 +309,8 @@ def td(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **kwa # backward iteration backdict = ss.copy() for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in kwargs, agents will use them! - backdict.update({k: v[t,...] for k, v in kwargs.items()}) + # be careful: if you include vars from self.back_iter_vars in shocked_vars, agents will use them! + backdict.update({k: v[t,...] for k, v in shocked_vars.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -340,7 +359,18 @@ def td(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **kwa else: return {**aggregates, **aggregate_hetoutputs} - def jac(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved=False): + # TODO: Change shocked_vars to enter *not* through kwargs, and symmetrically change impulse_nonlinear + # then add the option to pass kwargs into the self.jacobian invocation below + def impulse_linear(self, ss, **shocked_paths): + # infer T from shocked_paths, check that all shocks have same length + shock_lengths = [x.shape[0] for x in shocked_paths.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (shocked_paths) are same length!') + T = shock_lengths[0] + + return self.jacobian(ss, T, list(shocked_paths.keys())).apply(shocked_paths) + + def jacobian(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index b32f640..78d7750 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -1,3 +1,4 @@ +import warnings import numpy as np from .support.simple_displacement import ignore, Displace, AccumulatedDerivative @@ -37,6 +38,24 @@ def __init__(self, f): def __repr__(self): return f"" + # TODO: Deprecated methods, to be removed! + def ss(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(*args, **kwargs) + + def td(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(*args, **kwargs) + + def jac(self, ss, T=None, shock_list=None): + if shock_list is None: + shock_list = [] + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, T=T, shocked_vars=shock_list) + def _output_in_ss_format(self, *args, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" @@ -45,7 +64,7 @@ def _output_in_ss_format(self, *args, **kwargs): else: return dict(zip(self.output_list, [misc.numeric_primitive(self.f(*args, **kwargs))])) - def ss(self, *args, **kwargs): + def steady_state(self, *args, **kwargs): # Wrap args and kwargs in Ignore/IgnoreVector classes to be passed into the function "f" args = [ignore(x) for x in args] kwargs = {k: ignore(v) for k, v in kwargs.items()} @@ -72,20 +91,23 @@ def _output_in_td_format(self, **kwargs_new): else: return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def td(self, ss, **kwargs): - kwargs_new = {} - for k, v in kwargs.items(): + def impulse_nonlinear(self, ss, **shocked_paths): + input_args = {} + for k, v in shocked_paths.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') - kwargs_new[k] = Displace(v, ss=ss.get(k, None), name=k) + input_args[k] = Displace(v, ss=ss.get(k, None), name=k) for k in self.input_list: - if k not in kwargs_new: - kwargs_new[k] = ignore(ss[k]) + if k not in input_args: + input_args[k] = ignore(ss[k]) - return self._output_in_td_format(**kwargs_new) + return self._output_in_td_format(**input_args) - def jac(self, ss, T=None, shock_list=[]): + def impulse_linear(self, ss, T=None, **shocked_paths): + return self.jacobian(ss, T=T, shocked_vars=list(shocked_paths.keys())).apply(shocked_paths) + + def jacobian(self, ss, T=None, shocked_vars=None): """Assemble nested dict of Jacobians Parameters @@ -108,7 +130,10 @@ def jac(self, ss, T=None, shock_list=[]): if zero """ - relevant_shocks = [i for i in self.inputs if i in shock_list] + if shocked_vars is None: + shocked_vars = [] + + relevant_shocks = [i for i in self.inputs if i in shocked_vars] # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 6b19621..143cbe9 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,7 +1,9 @@ +import warnings + from .. import nonlinear +from ..steady_state import steady_state from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict -from ..steady_state import steady_state from ..blocks.simple_block import simple @@ -48,7 +50,25 @@ def __init__(self, block_list, name, unknowns, targets, solver=None, solver_kwar self.outputs = (set.union(*(b.outputs for b in block_list)) | set(list(self.unknowns.keys()))) - set(self.targets) self.inputs = set.union(*(b.inputs for b in block_list)) - self.outputs - def ss(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **calibration): + # TODO: Deprecated methods, to be removed! + def ss(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(*args, **kwargs) + + def td(self, *args, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(*args, **kwargs) + + def jac(self, ss, T=None, shock_list=None, **kwargs): + if shock_list is None: + shock_list = [] + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, T, shock_list, **kwargs) + + def steady_state(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **calibration): if self.solver is None: raise RuntimeError("Cannot call the ss method on this SolvedBlock without specifying a solver.") else: @@ -56,15 +76,18 @@ def ss(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **cali consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, solver=self.solver, **self.solver_kwargs) - def td(self, ss, monotonic=False, returnindividual=False, verbose=False, **kwargs): + def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, verbose=False, **shocked_paths): # TODO: add H_U_factored caching of some kind # also, inefficient since we are repeatedly starting from the steady state, need option # to provide a guess (not a big deal with just SimpleBlocks, of course) return nonlinear.td_solve(ss, self.block_list, list(self.unknowns.keys()), self.targets, monotonic=monotonic, - returnindividual=returnindividual, verbose=verbose, **kwargs) - - def jac(self, ss, T, shock_list, output_list=None, save=False, use_saved=False): - relevant_shocks = [i for i in self.inputs if i in shock_list] + returnindividual=returnindividual, verbose=verbose, **shocked_paths) + + def impulse_linear(self, ss, T=None, **shocked_paths): + return self.jacobian(ss, T, list(shocked_paths.keys())).apply(shocked_paths) + + def jacobian(self, ss, T, shocked_vars, output_list=None, save=False, use_saved=False): + relevant_shocks = [i for i in self.inputs if i in shocked_vars] if not relevant_shocks: return JacobianDict({}) From 0c5043996b5af7c6c8f4814a678b49781b7a4889 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Feb 2021 20:07:42 -0600 Subject: [PATCH 033/288] Change shocks to be passed in explicitly as opposed to through **kwargs and add a deprecation for the previous convention --- sequence_jacobian/blocks/combined_block.py | 6 +++--- sequence_jacobian/blocks/het_block.py | 24 +++++++++++---------- sequence_jacobian/blocks/simple_block.py | 8 +++++-- sequence_jacobian/blocks/solved_block.py | 14 ++++++++---- sequence_jacobian/devtools/deprecate.py | 22 +++++++++++++++++++ sequence_jacobian/nonlinear.py | 25 ++++++++++++++-------- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index b918f54..e086578 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -61,12 +61,12 @@ def steady_state(self, **calibration): ss_partial_eq.update(eval_block_ss(block, ss_partial_eq)) return ss_partial_eq - def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True): + def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True, **kwargs): irf_nonlin_partial_eq = {k: ss[k] + v for k, v in shocked_paths.items()} for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} # To only return dynamic paths of variables that do not remain at their steady state value - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, **input_args).items() + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs).items() if not np.all(v == ss[k])}) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. @@ -111,7 +111,7 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, - **{k: ss[k] + v for k, v in exogenous.items()}, **kwargs) + shocked_paths={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. if in_deviations: diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index fc2d1af..2dec38b 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -5,6 +5,8 @@ from .. import utilities as utils from ..jacobian.classes import JacobianDict +from ..devtools.deprecate import deprecated_shock_input_convention + def het(exogenous, policy, backward, backward_init=None): def decorator(back_step_fun): @@ -249,7 +251,8 @@ def steady_state(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10 return ss - def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_paths=None, **shocked_vars): + def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindividual=False, + grid_paths=None, **kwargs): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in kwargs are constant at their ss values. Analog to SimpleBlock.td. @@ -267,7 +270,7 @@ def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_pa return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies - shocked_vars : dict of {str : array(T, ...)} + shocked_paths : dict of {str : array(T, ...)} all time-varying inputs here, with first dimension being time this must have same length T for all entries (all outputs will be calculated up to T) @@ -279,11 +282,12 @@ def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_pa if returnindividual = True, additionally time paths for distribution and for all outputs of self.back_Step_fun on the full grid """ + shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) - # infer T from shocked_vars, check that all shocks have same length - shock_lengths = [x.shape[0] for x in shocked_vars.values()] + # infer T from shocked_paths, check that all shocks have same length + shock_lengths = [x.shape[0] for x in shocked_paths.values()] if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (shocked_vars) are same length!') + raise ValueError('Not all shocks in kwargs (shocked_paths) are same length!') T = shock_lengths[0] # copy from ss info @@ -309,8 +313,8 @@ def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_pa # backward iteration backdict = ss.copy() for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in shocked_vars, agents will use them! - backdict.update({k: v[t,...] for k, v in shocked_vars.items()}) + # be careful: if you include vars from self.back_iter_vars in shocked_paths, agents will use them! + backdict.update({k: v[t,...] for k, v in shocked_paths.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -359,16 +363,14 @@ def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, grid_pa else: return {**aggregates, **aggregate_hetoutputs} - # TODO: Change shocked_vars to enter *not* through kwargs, and symmetrically change impulse_nonlinear - # then add the option to pass kwargs into the self.jacobian invocation below - def impulse_linear(self, ss, **shocked_paths): + def impulse_linear(self, ss, shocked_paths, **kwargs): # infer T from shocked_paths, check that all shocks have same length shock_lengths = [x.shape[0] for x in shocked_paths.values()] if shock_lengths[1:] != shock_lengths[:-1]: raise ValueError('Not all shocks in kwargs (shocked_paths) are same length!') T = shock_lengths[0] - return self.jacobian(ss, T, list(shocked_paths.keys())).apply(shocked_paths) + return self.jacobian(ss, T, list(shocked_paths.keys()), **kwargs).apply(shocked_paths) def jacobian(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 78d7750..85d2ed6 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -5,6 +5,8 @@ from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc +from ..devtools.deprecate import deprecated_shock_input_convention + '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -91,7 +93,9 @@ def _output_in_td_format(self, **kwargs_new): else: return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def impulse_nonlinear(self, ss, **shocked_paths): + def impulse_nonlinear(self, ss, shocked_paths=None, **kwargs): + shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + input_args = {} for k, v in shocked_paths.items(): if np.isscalar(v): @@ -104,7 +108,7 @@ def impulse_nonlinear(self, ss, **shocked_paths): return self._output_in_td_format(**input_args) - def impulse_linear(self, ss, T=None, **shocked_paths): + def impulse_linear(self, ss, shocked_paths, T=None): return self.jacobian(ss, T=T, shocked_vars=list(shocked_paths.keys())).apply(shocked_paths) def jacobian(self, ss, T=None, shocked_vars=None): diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 143cbe9..7abc720 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -6,6 +6,8 @@ from ..jacobian.classes import JacobianDict from ..blocks.simple_block import simple +from ..devtools.deprecate import deprecated_shock_input_convention + def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: @@ -76,14 +78,18 @@ def steady_state(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=Fal consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, solver=self.solver, **self.solver_kwargs) - def impulse_nonlinear(self, ss, monotonic=False, returnindividual=False, verbose=False, **shocked_paths): + def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, + returnindividual=False, verbose=False, **kwargs): + shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + # TODO: add H_U_factored caching of some kind # also, inefficient since we are repeatedly starting from the steady state, need option # to provide a guess (not a big deal with just SimpleBlocks, of course) - return nonlinear.td_solve(ss, self.block_list, list(self.unknowns.keys()), self.targets, monotonic=monotonic, - returnindividual=returnindividual, verbose=verbose, **shocked_paths) + return nonlinear.td_solve(ss, self.block_list, list(self.unknowns.keys()), self.targets, + shocked_paths=shocked_paths, monotonic=monotonic, + returnindividual=returnindividual, verbose=verbose) - def impulse_linear(self, ss, T=None, **shocked_paths): + def impulse_linear(self, ss, shocked_paths, T=None): return self.jacobian(ss, T, list(shocked_paths.keys())).apply(shocked_paths) def jacobian(self, ss, T, shocked_vars, output_list=None, save=False, use_saved=False): diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py index 2784653..c77fe92 100644 --- a/sequence_jacobian/devtools/deprecate.py +++ b/sequence_jacobian/devtools/deprecate.py @@ -1,5 +1,27 @@ """Tools for deprecating older SSJ code conventions in favor of newer conventions""" +import warnings + # The code in this module is meant to assist with users who have used past versions of sequence-jacobian, by temporarily # providing support for old conventions via deprecated methods, providing time to allow for a seamless upgrade # to newer versions sequence-jacobian. + +# TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions +# themselves. + + +# For impulse_nonlinear, td_solve, and td_map +def deprecated_shock_input_convention(shocked_paths, kwargs): + if kwargs: + warnings.warn("Passing shock paths through kwargs is deprecated. Please explicitly specify" + " the dict of shocks in the keyword argument `shocked_paths`.", DeprecationWarning) + if shocked_paths is None: + return kwargs + else: + # If for whatever reason kwargs and shocked_paths is non-empty? + shocked_paths.update(kwargs) + else: + if shocked_paths is None: + return {} + else: + return shocked_paths diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index e28bc94..de1db5e 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -7,8 +7,10 @@ from .jacobian.drivers import get_H_U from .jacobian.support import pack_vectors, unpack_vectors +from .devtools.deprecate import deprecated_shock_input_convention -def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, monotonic=False, + +def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_U_factored=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, save=False, use_saved=False, grid_paths=None, **kwargs): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. @@ -21,6 +23,7 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon block_list : list, blocks in model (SimpleBlocks or HetBlocks) unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) + shocked_paths : dict, all shocked Z go here, must all have same length T H_U : [optional] array (nU*nU), Jacobian of targets with respect to unknowns H_U_factored : [optional] tuple, LU decomposition of H_U, save time by supplying this from utils.misc.factor() monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k @@ -31,19 +34,20 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon verbose : [optional] bool, flag to print largest absolute error for each target save : [optional] bool, flag for saving Jacobians inside HetBlocks during calc of H_U use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks during calc of H_U - kwargs : dict, all shocked Z go here, must all have same length T Returns ---------- results : dict, return paths for all aggregate variables, plus individual outcomes of HetBlock if returnindividual """ - # check to make sure that kwargs are valid shocks + shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + + # check to make sure that shocked_paths are valid shocks for x in unknowns + targets: - if x in kwargs: + if x in shocked_paths: raise ValueError(f'Shock {x} in td_solve cannot also be an unknown or target!') - # infer T from a single shocked Z in kwargs - for v in kwargs.values(): + # infer T from a single shocked Z in shocked_paths + for v in shocked_paths.values(): T = v.shape[0] break @@ -63,7 +67,8 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon # iterate until convergence for it in range(maxit): - results = td_map(ss, block_list, sort, monotonic, returnindividual, grid_paths=grid_paths, **kwargs, **Us) + results = td_map(ss, block_list, sort=sort, monotonic=monotonic, returnindividual=returnindividual, + grid_paths=grid_paths, **shocked_paths, **Us) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: print(f'On iteration {it}') @@ -82,11 +87,13 @@ def td_solve(ss, block_list, unknowns, targets, H_U=None, H_U_factored=None, mon return results -def td_map(ss, block_list, sort=None, monotonic=False, returnindividual=False, grid_paths=None, **kwargs): +def td_map(ss, block_list, shocked_paths=None, sort=None, monotonic=False, returnindividual=False, + grid_paths=None, **kwargs): """Helper for td_solve, calculates H(U, Z), where U and Z are in kwargs. Goes through block_list, topologically sorts the implied DAG, calculates H(U, Z), with missing paths always being interpreted as remaining at the steady state for a particular variable""" + shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) hetoptions = {'monotonic': monotonic, 'returnindividual': returnindividual, 'grid_paths': grid_paths} @@ -100,7 +107,7 @@ def td_map(ss, block_list, sort=None, monotonic=False, returnindividual=False, g # TODO: Rename the various references to kwargs/results to be more informative # if we do end up keeping this top-level functionality for passing in variables # initialize results - results = kwargs + results = shocked_paths for n in sort: block = block_list[n] From f4ba52b3558fea2d8636bf0befc15f2ac947111d Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 09:28:20 -0600 Subject: [PATCH 034/288] Harmonize some names amongst the CombinedBlock methods --- sequence_jacobian/blocks/combined_block.py | 30 ++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index e086578..d213700 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -48,7 +48,7 @@ def __repr__(self): else: return f"" - def steady_state(self, **calibration): + def steady_state(self, calibration): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: @@ -68,6 +68,7 @@ def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True, **kwargs): # To only return dynamic paths of variables that do not remain at their steady state value irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs).items() if not np.all(v == ss[k])}) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. if in_deviations: @@ -75,12 +76,13 @@ def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True, **kwargs): else: return irf_nonlin_partial_eq - def impulse_linear(self, ss, shocked_paths, T=None, in_deviations=True): - shocked_vars = list(shocked_paths.keys()) + def impulse_linear(self, ss, exogenous_paths, T=None, in_deviations=True): + exogenous = list(exogenous_paths.keys()) if T is None: - T = len(list(shocked_paths.values())[0]) - J_partial_eq = self.jacobian(ss, T=T, shocked_vars=shocked_vars) - irf_lin_partial_eq = J_partial_eq.apply(shocked_paths) + T = len(list(exogenous_paths.values())[0]) + J_partial_eq = self.jacobian(ss, exogenous=exogenous, T=T) + irf_lin_partial_eq = J_partial_eq.apply(exogenous_paths) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. if in_deviations: @@ -88,13 +90,13 @@ def impulse_linear(self, ss, shocked_paths, T=None, in_deviations=True): else: return irf_lin_partial_eq - def jacobian(self, ss, T=None, shocked_vars=None, outputs=None, save=False, use_saved=False): - if shocked_vars is None: - return JacobianDict({}) - else: - curlyJs, required = curlyJ_sorted(self.blocks, shocked_vars, ss, T=T, save=save, use_saved=use_saved) - J_partial_eq = forward_accumulate(curlyJs, shocked_vars, outputs=outputs, required=required) - return J_partial_eq + def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_saved=False): + if exogenous is None: + exogenous = list(self.inputs) + + curlyJs, required = curlyJ_sorted(self.blocks, exogenous, ss, T=T, save=save, use_saved=use_saved) + J_partial_eq = forward_accumulate(curlyJs, exogenous, outputs=outputs, required=required) + return J_partial_eq def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwargs): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices @@ -112,6 +114,7 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, shocked_paths={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. if in_deviations: @@ -124,6 +127,7 @@ def solve_impulse_linear(self, ss, exogenous, unknowns, targets, T=None, in_devi T = len(list(exogenous.values())[0]) J_gen_eq = get_G(self.blocks, list(exogenous.keys()), unknowns, targets, T=T, ss=ss, **kwargs) irf_lin_gen_eq = J_gen_eq.apply(exogenous) + # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. if in_deviations: From d31e41604ca5cc7ec9af97c83d61e8a992db32e4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 10:15:23 -0600 Subject: [PATCH 035/288] Harmonize names/conventions across Block classes. Breaking changes detailed below: - .ss() now only takes **kwargs and not *args, officially removing one of the old ways of invoking .ss(). This is because .steady_state() now takes a single dict `calibration` as opposed to variable kwargs for input arguments, and there's no way of naming variable args and collecting into a dict to pass into .steady_state() - .td() now only takes **kwargs and a single dict `ss` instead of variable args for the same reason as the .ss() fix above --- sequence_jacobian/blocks/helper_block.py | 12 ++--- sequence_jacobian/blocks/het_block.py | 64 ++++++++++++------------ sequence_jacobian/blocks/simple_block.py | 45 ++++++++--------- sequence_jacobian/blocks/solved_block.py | 26 +++++----- sequence_jacobian/devtools/deprecate.py | 16 +++--- sequence_jacobian/nonlinear.py | 24 ++++----- sequence_jacobian/steady_state.py | 6 +-- tests/base/test_jacobian.py | 4 +- tests/base/test_simple_block.py | 10 ++-- 9 files changed, 100 insertions(+), 107 deletions(-) diff --git a/sequence_jacobian/blocks/helper_block.py b/sequence_jacobian/blocks/helper_block.py index 066c0c4..bbb93ef 100644 --- a/sequence_jacobian/blocks/helper_block.py +++ b/sequence_jacobian/blocks/helper_block.py @@ -30,17 +30,15 @@ def ss(self, *args, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) return self.steady_state(*args, **kwargs) - def _output_in_ss_format(self, *args, **kwargs): + def _output_in_ss_format(self, calibration, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" if len(self.output_list) > 1: - return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(*args, **kwargs)])) + return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(**calibration, **kwargs)])) else: - return dict(zip(self.output_list, [misc.numeric_primitive(self.f(*args, **kwargs))])) + return dict(zip(self.output_list, [misc.numeric_primitive(self.f(**calibration, **kwargs))])) # Currently does not use any of the machinery in SimpleBlock to deal with time displacements and hence # can handle non-scalar inputs. - def steady_state(self, *args, **kwargs): - args = [x for x in args] - kwargs = {k: v for k, v in kwargs.items()} - return self._output_in_ss_format(*args, **kwargs) + def steady_state(self, calibration, **kwargs): + return self._output_in_ss_format(calibration, **kwargs) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 2dec38b..dadeab2 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -163,25 +163,25 @@ def __repr__(self): ''' # TODO: Deprecated methods, to be removed! - def ss(self, *args, **kwargs): + def ss(self, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(*args, **kwargs) + return self.steady_state(kwargs) - def td(self, *args, **kwargs): + def td(self, ss, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", DeprecationWarning) - return self.impulse_nonlinear(*args, **kwargs) + return self.impulse_nonlinear(ss, **kwargs) - def jac(self, ss, T=None, shock_list=None, **kwargs): + def jac(self, ss, shock_list=None, T=None, **kwargs): if shock_list is None: - shock_list = [] + shock_list = list(self.inputs) warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", DeprecationWarning) - return self.jacobian(ss, T, shock_list, **kwargs) + return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000, - hetoutput=False, **kwargs): + def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, + forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. Parameters @@ -218,16 +218,16 @@ def steady_state(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10 - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars """ - ss = copy.deepcopy(kwargs) + ss = copy.deepcopy(calibration) - # extract information from kwargs - Pi = kwargs[self.exogenous] - grid = {k: kwargs[k+'_grid'] for k in self.policy} - D_seed = kwargs.get('D', None) - pi_seed = kwargs.get(self.exogenous + '_seed', None) + # extract information from calibration + Pi = calibration[self.exogenous] + grid = {k: calibration[k+'_grid'] for k in self.policy} + D_seed = calibration.get('D', None) + pi_seed = calibration.get(self.exogenous + '_seed', None) # run backward iteration - sspol = self.policy_ss(kwargs, tol=backward_tol, maxit=backward_maxit) + sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) ss.update(sspol) # run forward iteration @@ -251,7 +251,7 @@ def steady_state(self, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10 return ss - def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindividual=False, + def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=False, grid_paths=None, **kwargs): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in @@ -270,7 +270,7 @@ def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindiv return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies - shocked_paths : dict of {str : array(T, ...)} + exogenous : dict of {str : array(T, ...)} all time-varying inputs here, with first dimension being time this must have same length T for all entries (all outputs will be calculated up to T) @@ -282,12 +282,12 @@ def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindiv if returnindividual = True, additionally time paths for distribution and for all outputs of self.back_Step_fun on the full grid """ - shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + exogenous = deprecated_shock_input_convention(exogenous, kwargs) - # infer T from shocked_paths, check that all shocks have same length - shock_lengths = [x.shape[0] for x in shocked_paths.values()] + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (shocked_paths) are same length!') + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] # copy from ss info @@ -313,8 +313,8 @@ def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindiv # backward iteration backdict = ss.copy() for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in shocked_paths, agents will use them! - backdict.update({k: v[t,...] for k, v in shocked_paths.items()}) + # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! + backdict.update({k: v[t,...] for k, v in exogenous.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -363,16 +363,16 @@ def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, returnindiv else: return {**aggregates, **aggregate_hetoutputs} - def impulse_linear(self, ss, shocked_paths, **kwargs): - # infer T from shocked_paths, check that all shocks have same length - shock_lengths = [x.shape[0] for x in shocked_paths.values()] + def impulse_linear(self, ss, exogenous, **kwargs): + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (shocked_paths) are same length!') + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] - return self.jacobian(ss, T, list(shocked_paths.keys()), **kwargs).apply(shocked_paths) + return self.jacobian(ss, T, list(exogenous.keys()), **kwargs).apply(exogenous) - def jacobian(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_saved=False): + def jacobian(self, ss, exogenous, T, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -381,7 +381,7 @@ def jacobian(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_ all steady-state info, intended to be from .ss() T : [optional] int number of time periods for T*T Jacobian - shock_list : list of str + exogenous : list of str names of input variables to differentiate wrt (main cost scales with # of inputs) output_list : list of str names of output variables to get derivatives of, if not provided assume all outputs of @@ -404,7 +404,7 @@ def jacobian(self, ss, T, shock_list, output_list=None, h=1E-4, save=False, use_ if output_list is None: output_list = self.non_back_iter_outputs - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in shock_list] + relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] # if we're supposed to use saved Jacobian, extract T-by-T submatrices for each (o,i) if use_saved: diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 85d2ed6..c288cac 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -41,14 +41,14 @@ def __repr__(self): return f"" # TODO: Deprecated methods, to be removed! - def ss(self, *args, **kwargs): + def ss(self, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(*args, **kwargs) + return self.steady_state(kwargs) - def td(self, *args, **kwargs): + def td(self, ss, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", DeprecationWarning) - return self.impulse_nonlinear(*args, **kwargs) + return self.impulse_nonlinear(ss, exogenous=kwargs) def jac(self, ss, T=None, shock_list=None): if shock_list is None: @@ -56,22 +56,19 @@ def jac(self, ss, T=None, shock_list=None): warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", DeprecationWarning) - return self.jacobian(ss, T=T, shocked_vars=shock_list) + return self.jacobian(ss, exogenous=shock_list, T=T) - def _output_in_ss_format(self, *args, **kwargs): + def _output_in_ss_format(self, **kwargs): """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single numeric primitive, as opposed to Ignore/IgnoreVector objects""" if len(self.output_list) > 1: - return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(*args, **kwargs)])) + return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(**kwargs)])) else: - return dict(zip(self.output_list, [misc.numeric_primitive(self.f(*args, **kwargs))])) + return dict(zip(self.output_list, [misc.numeric_primitive(self.f(**kwargs))])) - def steady_state(self, *args, **kwargs): - # Wrap args and kwargs in Ignore/IgnoreVector classes to be passed into the function "f" - args = [ignore(x) for x in args] - kwargs = {k: ignore(v) for k, v in kwargs.items()} - - return self._output_in_ss_format(*args, **kwargs) + def steady_state(self, calibration, **kwargs): + input_args = {k: ignore(v) for k, v in calibration.items()} + return self._output_in_ss_format(**input_args, **kwargs) def _output_in_td_format(self, **kwargs_new): """Returns output of the method td as a dict mapping output names to numeric primitives (scalars/vectors) @@ -93,11 +90,11 @@ def _output_in_td_format(self, **kwargs_new): else: return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def impulse_nonlinear(self, ss, shocked_paths=None, **kwargs): - shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + def impulse_nonlinear(self, ss, exogenous=None, **kwargs): + exogenous = deprecated_shock_input_convention(exogenous, kwargs) input_args = {} - for k, v in shocked_paths.items(): + for k, v in exogenous.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') input_args[k] = Displace(v, ss=ss.get(k, None), name=k) @@ -108,10 +105,10 @@ def impulse_nonlinear(self, ss, shocked_paths=None, **kwargs): return self._output_in_td_format(**input_args) - def impulse_linear(self, ss, shocked_paths, T=None): - return self.jacobian(ss, T=T, shocked_vars=list(shocked_paths.keys())).apply(shocked_paths) + def impulse_linear(self, ss, exogenous, T=None): + return self.jacobian(ss, T=T, exogenous=list(exogenous.keys())).apply(exogenous) - def jacobian(self, ss, T=None, shocked_vars=None): + def jacobian(self, ss, exogenous=None, T=None): """Assemble nested dict of Jacobians Parameters @@ -121,7 +118,7 @@ def jacobian(self, ss, T=None, shocked_vars=None): T : int, optional number of time periods for explicit T*T Jacobian if omitted, more efficient SimpleSparse objects returned - shock_list : list of str, optional + exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs h : float, optional radius for symmetric numerical differentiation @@ -134,10 +131,10 @@ def jacobian(self, ss, T=None, shocked_vars=None): if zero """ - if shocked_vars is None: - shocked_vars = [] + if exogenous is None: + exogenous = list(self.inputs) - relevant_shocks = [i for i in self.inputs if i in shocked_vars] + relevant_shocks = [i for i in self.inputs if i in exogenous] # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 7abc720..31b928f 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -53,14 +53,14 @@ def __init__(self, block_list, name, unknowns, targets, solver=None, solver_kwar self.inputs = set.union(*(b.inputs for b in block_list)) - self.outputs # TODO: Deprecated methods, to be removed! - def ss(self, *args, **kwargs): + def ss(self, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(*args, **kwargs) + return self.steady_state(kwargs) - def td(self, *args, **kwargs): + def td(self, ss, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", DeprecationWarning) - return self.impulse_nonlinear(*args, **kwargs) + return self.impulse_nonlinear(ss, **kwargs) def jac(self, ss, T=None, shock_list=None, **kwargs): if shock_list is None: @@ -68,9 +68,9 @@ def jac(self, ss, T=None, shock_list=None, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", DeprecationWarning) - return self.jacobian(ss, T, shock_list, **kwargs) + return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **calibration): + def steady_state(self, calibration, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False): if self.solver is None: raise RuntimeError("Cannot call the ss method on this SolvedBlock without specifying a solver.") else: @@ -78,22 +78,22 @@ def steady_state(self, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=Fal consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, solver=self.solver, **self.solver_kwargs) - def impulse_nonlinear(self, ss, shocked_paths=None, monotonic=False, + def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=False, verbose=False, **kwargs): - shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + exogenous = deprecated_shock_input_convention(exogenous, kwargs) # TODO: add H_U_factored caching of some kind # also, inefficient since we are repeatedly starting from the steady state, need option # to provide a guess (not a big deal with just SimpleBlocks, of course) return nonlinear.td_solve(ss, self.block_list, list(self.unknowns.keys()), self.targets, - shocked_paths=shocked_paths, monotonic=monotonic, + exogenous=exogenous, monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - def impulse_linear(self, ss, shocked_paths, T=None): - return self.jacobian(ss, T, list(shocked_paths.keys())).apply(shocked_paths) + def impulse_linear(self, ss, exogenous, T=None): + return self.jacobian(ss, T, list(exogenous.keys())).apply(exogenous) - def jacobian(self, ss, T, shocked_vars, output_list=None, save=False, use_saved=False): - relevant_shocks = [i for i in self.inputs if i in shocked_vars] + def jacobian(self, ss, exogenous, T, output_list=None, save=False, use_saved=False): + relevant_shocks = [i for i in self.inputs if i in exogenous] if not relevant_shocks: return JacobianDict({}) diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py index c77fe92..c506fa3 100644 --- a/sequence_jacobian/devtools/deprecate.py +++ b/sequence_jacobian/devtools/deprecate.py @@ -11,17 +11,17 @@ # For impulse_nonlinear, td_solve, and td_map -def deprecated_shock_input_convention(shocked_paths, kwargs): +def deprecated_shock_input_convention(exogenous, kwargs): if kwargs: - warnings.warn("Passing shock paths through kwargs is deprecated. Please explicitly specify" - " the dict of shocks in the keyword argument `shocked_paths`.", DeprecationWarning) - if shocked_paths is None: + warnings.warn("Passing shock paths/exogenous through kwargs is deprecated. Please explicitly specify" + " the dict of shocks in the keyword argument `exogenous`.", DeprecationWarning) + if exogenous is None: return kwargs else: - # If for whatever reason kwargs and shocked_paths is non-empty? - shocked_paths.update(kwargs) + # If for whatever reason kwargs and exogenous is non-empty? + exogenous.update(kwargs) else: - if shocked_paths is None: + if exogenous is None: return {} else: - return shocked_paths + return exogenous diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index de1db5e..9970079 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -10,7 +10,7 @@ from .devtools.deprecate import deprecated_shock_input_convention -def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_U_factored=None, monotonic=False, +def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_factored=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, save=False, use_saved=False, grid_paths=None, **kwargs): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. @@ -23,7 +23,7 @@ def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_ block_list : list, blocks in model (SimpleBlocks or HetBlocks) unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) - shocked_paths : dict, all shocked Z go here, must all have same length T + exogenous : dict, all shocked Z go here, must all have same length T H_U : [optional] array (nU*nU), Jacobian of targets with respect to unknowns H_U_factored : [optional] tuple, LU decomposition of H_U, save time by supplying this from utils.misc.factor() monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k @@ -39,15 +39,15 @@ def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_ ---------- results : dict, return paths for all aggregate variables, plus individual outcomes of HetBlock if returnindividual """ - shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + exogenous = deprecated_shock_input_convention(exogenous, kwargs) - # check to make sure that shocked_paths are valid shocks + # check to make sure that exogenous are valid shocks for x in unknowns + targets: - if x in shocked_paths: + if x in exogenous: raise ValueError(f'Shock {x} in td_solve cannot also be an unknown or target!') - # infer T from a single shocked Z in shocked_paths - for v in shocked_paths.values(): + # infer T from a single shocked Z in exogenous + for v in exogenous.values(): T = v.shape[0] break @@ -68,7 +68,7 @@ def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_ # iterate until convergence for it in range(maxit): results = td_map(ss, block_list, sort=sort, monotonic=monotonic, returnindividual=returnindividual, - grid_paths=grid_paths, **shocked_paths, **Us) + grid_paths=grid_paths, **exogenous, **Us) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: print(f'On iteration {it}') @@ -87,13 +87,13 @@ def td_solve(ss, block_list, unknowns, targets, shocked_paths=None, H_U=None, H_ return results -def td_map(ss, block_list, shocked_paths=None, sort=None, monotonic=False, returnindividual=False, +def td_map(ss, block_list, exogenous=None, sort=None, monotonic=False, returnindividual=False, grid_paths=None, **kwargs): """Helper for td_solve, calculates H(U, Z), where U and Z are in kwargs. Goes through block_list, topologically sorts the implied DAG, calculates H(U, Z), with missing paths always being interpreted as remaining at the steady state for a particular variable""" - shocked_paths = deprecated_shock_input_convention(shocked_paths, kwargs) + exogenous = deprecated_shock_input_convention(exogenous, kwargs) hetoptions = {'monotonic': monotonic, 'returnindividual': returnindividual, 'grid_paths': grid_paths} @@ -107,7 +107,7 @@ def td_map(ss, block_list, shocked_paths=None, sort=None, monotonic=False, retur # TODO: Rename the various references to kwargs/results to be more informative # if we do end up keeping this top-level functionality for passing in variables # initialize results - results = shocked_paths + results = exogenous for n in sort: block = block_list[n] @@ -121,5 +121,3 @@ def td_map(ss, block_list, shocked_paths=None, sort=None, monotonic=False, retur results.update(block.td(ss, **blockoptions, **{k: results[k] for k in block.inputs if k in results})) return results - - diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state.py index cbe5dc9..3c989f8 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state.py @@ -184,10 +184,10 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol # Simple and HetBlocks require different handling of block.ss() output since # SimpleBlocks return a tuple of un-labeled arguments, whereas HetBlocks return dictionaries if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock) or isinstance(block, HetBlock): - outputs = block.ss(**input_args, **kwargs) + outputs = block.steady_state(input_args, **kwargs) else: # since .ss for SolvedBlocks calls the steady_state driver function - outputs = block.ss(**input_args, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) + outputs = block.steady_state(input_args, consistency_check=consistency_check, + ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) return outputs diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index dc5af0b..54b1378 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -65,7 +65,7 @@ def test_fake_news_v_actual(one_asset_hank_model): household = blocks[0] T = 40 shock_list=['w', 'r', 'Div', 'Tax'] - Js = household.jac(ss, T, shock_list) + Js = household.jac(ss, shock_list, T) output_list = household.non_back_iter_outputs # Preliminary processing of the steady state @@ -132,7 +132,7 @@ def test_fake_news_v_direct_method(one_asset_hank_model): output_list = household.non_back_iter_outputs h = 1E-4 - Js = household.jac(ss, T, shock_list) + Js = household.jac(ss, shock_list, T) Js_direct = {o.capitalize(): {i: np.empty((T, T)) for i in shock_list} for o in output_list} # run td once without any shocks to get paths to subtract against diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 6714476..32025a4 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -27,17 +27,17 @@ def taylor(r, pi, phi): return i -@pytest.mark.parametrize("block,ss", [(F, (1, 1, 1, 0.5)), - (investment, (1, 1, 0.05, 1, 1, 1, 0.05, 2, 0.5)), - (taylor, (0.05, 0.01, 1.5))]) +@pytest.mark.parametrize("block,ss", [(F, {"K": 1, "L": 1, "Z": 1, "alpha": 0.5}), + (investment, {"Q": 1, "K": 1, "r": 0.05, "N": 1, "mc": 1, "Z": 1, "delta": 0.05, + "epsI": 2, "alpha": 0.5}), + (taylor, {"r": 0.05, "pi": 0.01, "phi": 1.5})]) def test_block_consistency(block, ss): """Make sure ss, td, and jac methods are all consistent with each other. Requires that all inputs of simple block allow calculating Jacobians""" # get ss output - ss_results = block.ss(*ss) + ss_results = block.ss(**ss) # now if we put in constant inputs, td should give us the same! - ss = dict(zip(block.input_list, ss)) td_results = block.td(ss, **{k: np.full(20, v) for k, v in ss.items()}) for k, v in td_results.items(): assert np.all(v == ss_results[k]) From 571a6c23aa3fc82cc0988417b53e9518808e6f35 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 10:54:21 -0600 Subject: [PATCH 036/288] Harmonize impulse_linear with impulse_nonlinear --- sequence_jacobian/blocks/combined_block.py | 45 ++++++++++++++-------- sequence_jacobian/blocks/het_block.py | 4 +- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/blocks/solved_block.py | 9 ++++- sequence_jacobian/jacobian/classes.py | 2 +- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index d213700..bde7ccf 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -6,8 +6,9 @@ from .. import utilities as utils from ..steady_state import eval_block_ss, steady_state, provide_solver_default from ..nonlinear import td_solve -from ..jacobian.drivers import get_G, forward_accumulate, curlyJ_sorted +from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict +from ..blocks.het_block import HetBlock def combine(*args, name="", model_alias=False): @@ -26,6 +27,8 @@ def __init__(self, *blocks, name="", model_alias=False): self.blocks_w_helpers = None self._sorted_indices_w_o_helpers = utils.graph.block_sort(blocks, ignore_helpers=True) + self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] if not name: @@ -61,13 +64,13 @@ def steady_state(self, calibration): ss_partial_eq.update(eval_block_ss(block, ss_partial_eq)) return ss_partial_eq - def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True, **kwargs): - irf_nonlin_partial_eq = {k: ss[k] + v for k, v in shocked_paths.items()} + def impulse_nonlinear(self, ss, exogenous, in_deviations=True, **kwargs): + irf_nonlin_partial_eq = {k: ss[k] + v for k, v in exogenous.items()} for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} - # To only return dynamic paths of variables that do not remain at their steady state value - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs).items() - if not np.all(v == ss[k])}) + + if input_args: # If this block is actually perturbed + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs).items()}) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. @@ -76,12 +79,13 @@ def impulse_nonlinear(self, ss, shocked_paths, in_deviations=True, **kwargs): else: return irf_nonlin_partial_eq - def impulse_linear(self, ss, exogenous_paths, T=None, in_deviations=True): - exogenous = list(exogenous_paths.keys()) - if T is None: - T = len(list(exogenous_paths.values())[0]) - J_partial_eq = self.jacobian(ss, exogenous=exogenous, T=T) - irf_lin_partial_eq = J_partial_eq.apply(exogenous_paths) + def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): + irf_lin_partial_eq = deepcopy(exogenous) + for block in self.blocks: + input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} + + if input_args: # If this block is actually perturbed + irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T).items()}) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. @@ -93,9 +97,20 @@ def impulse_linear(self, ss, exogenous_paths, T=None, in_deviations=True): def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_saved=False): if exogenous is None: exogenous = list(self.inputs) + if outputs is None: + outputs = self.outputs + + J_partial_eq = JacobianDict.identity(exogenous) + for block in self.blocks: + if isinstance(block, HetBlock): + curlyJ = block.jacobian(ss, exogenous, T, save=save, use_saved=use_saved).complete() + else: + curlyJ = block.jacobian(ss, exogenous, T).complete() + + # If we want specific list of outputs, restrict curlyJ to that before continuing + curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] + J_partial_eq.update(curlyJ.compose(J_partial_eq)) - curlyJs, required = curlyJ_sorted(self.blocks, exogenous, ss, T=T, save=save, use_saved=use_saved) - J_partial_eq = forward_accumulate(curlyJs, exogenous, outputs=outputs, required=required) return J_partial_eq def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwargs): @@ -113,7 +128,7 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, - shocked_paths={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) + exogenous={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index dadeab2..1210f9b 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -363,14 +363,14 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividua else: return {**aggregates, **aggregate_hetoutputs} - def impulse_linear(self, ss, exogenous, **kwargs): + def impulse_linear(self, ss, exogenous, T=None, **kwargs): # infer T from exogenous, check that all shocks have same length shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] - return self.jacobian(ss, T, list(exogenous.keys()), **kwargs).apply(exogenous) + return self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous) def jacobian(self, ss, exogenous, T, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index c288cac..a8c45b6 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -106,7 +106,7 @@ def impulse_nonlinear(self, ss, exogenous=None, **kwargs): return self._output_in_td_format(**input_args) def impulse_linear(self, ss, exogenous, T=None): - return self.jacobian(ss, T=T, exogenous=list(exogenous.keys())).apply(exogenous) + return self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous) def jacobian(self, ss, exogenous=None, T=None): """Assemble nested dict of Jacobians diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 31b928f..64588eb 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -90,7 +90,14 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=returnindividual, verbose=verbose) def impulse_linear(self, ss, exogenous, T=None): - return self.jacobian(ss, T, list(exogenous.keys())).apply(exogenous) + if T is None: + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + + return self.jacobian(ss, list(exogenous.keys()), T=T).apply(exogenous) def jacobian(self, ss, exogenous, T, output_list=None, save=False, use_saved=False): relevant_shocks = [i for i in self.inputs if i in exogenous] diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index e6257dd..0264b93 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -397,7 +397,7 @@ def apply(self, x): T = len(next(iter(x.values()))) inputs = x.keys() & set(self.inputs) - J_oi = self.nesteddict + J_oi = self.complete().nesteddict y = {} for o in self.outputs: From a88f155c7952e92e1c44fc22f1638d0ca40a1444 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 11:16:42 -0600 Subject: [PATCH 037/288] Tidy and document CombinedBlock --- sequence_jacobian/blocks/combined_block.py | 42 +++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index bde7ccf..bf5a7f5 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -17,18 +17,27 @@ def combine(*args, name="", model_alias=False): class CombinedBlock: + """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides + a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, + and calculates Jacobians along the DAG""" # To users: Do *not* manually change the attributes via assignment. Instantiating a # CombinedBlock has some automated features that are inferred from initial instantiation but not from # re-assignment of attributes post-instantiation. def __init__(self, *blocks, name="", model_alias=False): + # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. self._blocks_unsorted = blocks - self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated - self.blocks_w_helpers = None - + # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks + # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort + # Hence, we will cache that info upon first invocation of the steady_state self._sorted_indices_w_o_helpers = utils.graph.block_sort(blocks, ignore_helpers=True) + self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + # User-facing attributes for accessing blocks + # .blocks_w_helpers meant to only interface with steady_state functionality + # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) + self.blocks_w_helpers = None self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] if not name: @@ -43,6 +52,7 @@ def __init__(self, *blocks, name="", model_alias=False): all_inputs = set().union(*[block.inputs for block in self.blocks]) self.inputs = all_inputs.difference(self.outputs) + # If the create_model() is used instead of combine(), we will have __repr__ show this object as a 'Model' self._model_alias = model_alias def __repr__(self): @@ -52,6 +62,7 @@ def __repr__(self): return f"" def steady_state(self, calibration): + """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: @@ -65,6 +76,8 @@ def steady_state(self, calibration): return ss_partial_eq def impulse_nonlinear(self, ss, exogenous, in_deviations=True, **kwargs): + """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from + a steady state, `ss`""" irf_nonlin_partial_eq = {k: ss[k] + v for k, v in exogenous.items()} for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} @@ -80,6 +93,8 @@ def impulse_nonlinear(self, ss, exogenous, in_deviations=True, **kwargs): return irf_nonlin_partial_eq def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): + """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from + a steady_state, `ss`""" irf_lin_partial_eq = deepcopy(exogenous) for block in self.blocks: input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} @@ -95,6 +110,8 @@ def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): return irf_lin_partial_eq def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_saved=False): + """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at + a steady state, `ss`""" if exogenous is None: exogenous = list(self.inputs) if outputs is None: @@ -114,6 +131,9 @@ def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_sav return J_partial_eq def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwargs): + """Evaluate a general equilibrium steady state of the CombinedBlock given a `calibration` + and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and + the target conditions that must hold in general equilibrium""" # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: @@ -127,6 +147,9 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar return ss_gen_eq def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): + """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, exogenous={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) @@ -138,8 +161,16 @@ def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviation return irf_nonlin_gen_eq def solve_impulse_linear(self, ss, exogenous, unknowns, targets, T=None, in_deviations=True, **kwargs): + """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" if T is None: - T = len(list(exogenous.values())[0]) + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + J_gen_eq = get_G(self.blocks, list(exogenous.keys()), unknowns, targets, T=T, ss=ss, **kwargs) irf_lin_gen_eq = J_gen_eq.apply(exogenous) @@ -151,6 +182,9 @@ def solve_impulse_linear(self, ss, exogenous, unknowns, targets, T=None, in_devi return irf_lin_gen_eq def solve_jacobian(self, ss, exogenous, unknowns, targets, T=None, **kwargs): + """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks + at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" J_gen_eq = get_G(self.blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) return J_gen_eq From d42677c217f08367acd7ae58362703d4ad8bd76f Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 11:59:08 -0600 Subject: [PATCH 038/288] Make more HetBlock utilities available from the top-level --- sequence_jacobian/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 3e70626..fcb2aa4 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -18,9 +18,10 @@ from .steady_state import steady_state from .jacobian.drivers import get_G, get_H_U, get_impulse from .nonlinear import td_solve -from .utilities import discretize -from .utilities import interpolate -from .utilities.discretize import agrid + +# Useful utilities for setting up HetBlocks +from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen +from .utilities.interpolate import interpolate_y from .utilities.optimized_routines import setmin # Ensure warning uniformity across package From 01b45973a53bbb246f3f0efc7c6f45b8bbbb02d7 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 19 Feb 2021 12:14:30 -0600 Subject: [PATCH 039/288] Make debug available from within devtools sub-package --- sequence_jacobian/devtools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/devtools/__init__.py b/sequence_jacobian/devtools/__init__.py index b9332f6..9795de6 100644 --- a/sequence_jacobian/devtools/__init__.py +++ b/sequence_jacobian/devtools/__init__.py @@ -1,3 +1,3 @@ """Tools for debugging, developing, and deprecating code""" -from . import analysis, deprecate, upgrade +from . import analysis, debug, deprecate, upgrade From 25581b5ab4620bbdddd67f9903d9b5192c4675f4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 10:26:34 -0600 Subject: [PATCH 040/288] Add additional accuracy specifications for steady_state --- sequence_jacobian/steady_state.py | 41 +++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state.py index 3c989f8..fa93eb9 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state.py @@ -1,5 +1,6 @@ """A general function for computing a model's steady state variables and parameters values""" +import warnings import numpy as np import scipy.optimize as opt from copy import deepcopy @@ -14,8 +15,9 @@ # Find the steady state solution def steady_state(blocks, calibration, unknowns, targets, - consistency_check=True, ttol=1e-9, ctol=1e-9, - verbose=False, solver=None, solver_kwargs=None, + consistency_check=True, ttol=2e-12, ctol=1e-9, + backward_tol=1e-8, forward_tol=1e-10, + verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. @@ -36,6 +38,12 @@ def steady_state(blocks, calibration, unknowns, targets, ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the use of HelperBlocks, to equal the desired values + backward_tol/forward_tol: `float` + See `HetBlock` .ss method docstring for details on these additional convergence tolerances + verbose: `bool` + Display the content of optional print statements within the solver for more responsive feedback + fragile: `bool` + Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails solver: `string` The name of the numerical solver that the user would like to user. Can either be a custom solver the user implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root @@ -73,7 +81,8 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False continue else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose) + ttol=ttol, ctol=ctol, backward_tol=backward_tol, + forward_tol=forward_tol, verbose=verbose) if include_helpers and isinstance(blocks[i], HelperBlock): helper_outputs.update(outputs) ss_values.update(outputs) @@ -99,7 +108,8 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check: - assert abs(np.max(residual(unknown_solutions, include_helpers=False))) < ctol + cresid = abs(np.max(residual(unknown_solutions, include_helpers=False))) + run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns ss_values.update(zip(unknowns, utils.misc.make_tuple(unknown_solutions))) @@ -111,6 +121,20 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False return ss_values +def run_consistency_check(cresid, ctol=1e-9, fragile=False): + if cresid > ctol: + if fragile: + raise RuntimeError(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + else: + warnings.warn(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + + def find_target_block(blocks, target): for block in blocks: if target in blocks.output: @@ -171,7 +195,8 @@ def compute_target_values(targets, potential_args): # Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. -def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **kwargs): +def eval_block_ss(block, potential_args, consistency_check=True, ttol=2e-12, ctol=1e-9, + backward_tol=1e-8, forward_tol=1e-10, verbose=False, **kwargs): """ Evaluate the .ss method of a block, given a dictionary of potential arguments. @@ -183,8 +208,10 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol # Simple and HetBlocks require different handling of block.ss() output since # SimpleBlocks return a tuple of un-labeled arguments, whereas HetBlocks return dictionaries - if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock) or isinstance(block, HetBlock): + if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock): outputs = block.steady_state(input_args, **kwargs) + elif isinstance(block, HetBlock): + outputs = block.steady_state(input_args, backward_tol=backward_tol, forward_tol=forward_tol, **kwargs) else: # since .ss for SolvedBlocks calls the steady_state driver function outputs = block.steady_state(input_args, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) @@ -194,7 +221,7 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, constrained_method, constrained_kwargs, - tol=1e-9, verbose=False): + tol=2e-12, verbose=False): """ Given a residual function (constructed within steady_state) and a set of bounds or initial values for the set of unknowns, solve for the root. From 5cc481ac762906bc39fdadb116d41426759d1910 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 17:09:24 -0600 Subject: [PATCH 041/288] Create steady_state sub-package and implement first-pass at SteadyStateDict class --- sequence_jacobian/__init__.py | 2 +- sequence_jacobian/blocks/combined_block.py | 3 +- sequence_jacobian/blocks/solved_block.py | 2 +- sequence_jacobian/steady_state/__init__.py | 1 + sequence_jacobian/steady_state/classes.py | 41 ++++ .../drivers.py} | 212 ++---------------- sequence_jacobian/steady_state/support.py | 182 +++++++++++++++ 7 files changed, 245 insertions(+), 198 deletions(-) create mode 100644 sequence_jacobian/steady_state/__init__.py create mode 100644 sequence_jacobian/steady_state/classes.py rename sequence_jacobian/{steady_state.py => steady_state/drivers.py} (53%) create mode 100644 sequence_jacobian/steady_state/support.py diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index fcb2aa4..08e79c6 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -15,7 +15,7 @@ from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved -from .steady_state import steady_state +from .steady_state.drivers import steady_state from .jacobian.drivers import get_G, get_H_U, get_impulse from .nonlinear import td_solve diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index bf5a7f5..357de3d 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -4,7 +4,8 @@ import numpy as np from .. import utilities as utils -from ..steady_state import eval_block_ss, steady_state, provide_solver_default +from ..steady_state.drivers import eval_block_ss, steady_state +from ..steady_state.support import provide_solver_default from ..nonlinear import td_solve from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 64588eb..2e22daa 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,7 +1,7 @@ import warnings from .. import nonlinear -from ..steady_state import steady_state +from ..steady_state.drivers import steady_state from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict from ..blocks.simple_block import simple diff --git a/sequence_jacobian/steady_state/__init__.py b/sequence_jacobian/steady_state/__init__.py new file mode 100644 index 0000000..017e34b --- /dev/null +++ b/sequence_jacobian/steady_state/__init__.py @@ -0,0 +1 @@ +"""Steady state computation and support functions""" diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py new file mode 100644 index 0000000..d695b31 --- /dev/null +++ b/sequence_jacobian/steady_state/classes.py @@ -0,0 +1,41 @@ +"""Various classes to support the computation of steady states""" + +from copy import deepcopy +import numpy as np + + +class SteadyStateDict: + def __init__(self, raw_dict, blocks=None): + # TODO: Will need to re-think flat storage of data if/when we move to implementing remapping + self.data = deepcopy(raw_dict) + + if blocks is not None: + self.block_map = {b.name: b.outputs for b in blocks} + self.block_names = list(self.block_map.keys()) + + # Record the values in raw_dict not output by any of the blocks as part of the `calibration` + self.block_map["calibration"] = set(raw_dict.keys()) - set().union(*self.block_map.values()) + else: + self.block_map = {"calibration": set(raw_dict.keys())} + self.block_names = ["calibration"] + + def __repr__(self, raw=False): + if set(self.block_names) == {"calibration"} or raw: + return self.data.__repr__() + else: + return f"<{type(self).__name__} blocks={self.block_names}" + + def __getitem__(self, key): + if key in self.block_names: + return {k: v for k, v in self.data.items() if k in self.block_map[key]} + else: + return self.data[key] + + def __setitem__(self, key, value): + if key not in self.data: + block_name_to_assign = "calibration" + self.block_map[block_name_to_assign] = self.block_map[block_name_to_assign] | {key} + self.data[key] = value + + def aggregates(self): + return {k: v for k, v in self.data.items() if np.isscalar(v)} diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state/drivers.py similarity index 53% rename from sequence_jacobian/steady_state.py rename to sequence_jacobian/steady_state/drivers.py index 3c989f8..1760fa8 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -4,12 +4,13 @@ import scipy.optimize as opt from copy import deepcopy -from numbers import Real -from . import utilities as utils -from .utilities.misc import unprime, dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs -from .blocks.simple_block import SimpleBlock -from .blocks.helper_block import HelperBlock -from .blocks.het_block import HetBlock +from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ + extract_univariate_initial_values_or_bounds, constrained_multivariate_residual +from ..utilities import solvers, graph +from ..utilities.misc import unprime, dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs, make_tuple +from ..blocks.simple_block import SimpleBlock +from ..blocks.helper_block import HelperBlock +from ..blocks.het_block import HetBlock # Find the steady state solution @@ -59,7 +60,7 @@ def steady_state(blocks, calibration, unknowns, targets, constrained_kwargs = {} ss_values = deepcopy(calibration) - topsorted = utils.graph.block_sort(blocks, calibration=calibration) + topsorted = graph.block_sort(blocks, calibration=calibration) def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False): ss_values.update(smart_zip(unknowns.keys(), unknown_values)) @@ -102,7 +103,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False assert abs(np.max(residual(unknown_solutions, include_helpers=False))) < ctol # Update to set the solutions for the steady state values of the unknowns - ss_values.update(zip(unknowns, utils.misc.make_tuple(unknown_solutions))) + ss_values.update(zip(unknowns, make_tuple(unknown_solutions))) # Find the hetoutputs of the Hetblocks that have hetoutputs for i in find_blocks_with_hetoutputs(blocks): @@ -111,65 +112,6 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False return ss_values -def find_target_block(blocks, target): - for block in blocks: - if target in blocks.output: - return block - - -def provide_solver_default(unknowns): - if len(unknowns) == 1: - bounds = list(unknowns.values())[0] - if not isinstance(bounds, tuple) or bounds[0] > bounds[1]: - raise ValueError("Unable to find a compatible one-dimensional solver with provided `unknowns`.\n" - " Please provide valid lower/upper bounds, e.g. unknowns = {`a`: (0, 1)}") - else: - return "brentq" - elif len(unknowns) > 1: - init_values = list(unknowns.values()) - if not np.all([isinstance(v, Real) for v in init_values]): - raise ValueError("Unable to find a compatible multi-dimensional solver with provided `unknowns`.\n" - " Please provide valid initial values, e.g. unknowns = {`a`: 1, `b`: 2}") - else: - return "broyden_custom" - else: - raise ValueError("`unknowns` is empty! Please provide a dict of keys/values equal to the number of unknowns" - " that need to be solved for.") - - - -# Allow targets to be specified in the following formats -# 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) -# 2) target = {"r": 0.01} (allowing for the target to be non-zero) -# 3) target = {"K": "A"} (allowing the target to be another variable in potential_args) -def compute_target_values(targets, potential_args): - """ - For a given set of target specifications and potential arguments available, compute the targets. - Called as the return value for the residual function when utilizing the numerical solver. - - targets: Refer to `steady_state` function docstring - potential_args: Refer to the `steady_state` function docstring for the "calibration" variable - - return: A `float` (if computing a univariate target) or an `np.ndarray` (if using a multivariate target) - """ - target_values = np.empty(len(targets)) - for (i, t) in enumerate(targets): - v = targets[t] if isinstance(targets, dict) else 0 - if type(v) == str: - target_values[i] = potential_args[t] - potential_args[v] - else: - target_values[i] = potential_args[t] - v - # TODO: Implement feature to allow for an arbitrary explicit function expression as a potential target value - # e.g. targets = {"goods_mkt": "Y - C - I"}, so long as the expression is only comprise of generic numerical - # operators and variables solved for along the DAG prior to reaching the target. - - # Univariate solvers require float return values (and not lists) - if len(targets) == 1: - return target_values[0] - else: - return target_values - - # Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **kwargs): """ @@ -248,27 +190,27 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = utils.solvers.broyden_solver(residual, initial_values, tol=tol, - verbose=verbose, **solver_kwargs) + unknown_solutions, _ = solvers.broyden_solver(residual, initial_values, tol=tol, + verbose=verbose, **solver_kwargs) else: constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) - unknown_solutions, _ = utils.solvers.broyden_solver(constrained_residual, initial_values, - verbose=verbose, tol=tol, **solver_kwargs) + unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, + verbose=verbose, tol=tol, **solver_kwargs) unknown_solutions = list(unknown_solutions) elif solver == "newton_custom": initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = utils.solvers.newton_solver(residual, initial_values, tol=tol, - verbose=verbose, **solver_kwargs) + unknown_solutions, _ = solvers.newton_solver(residual, initial_values, tol=tol, + verbose=verbose, **solver_kwargs) else: constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) - unknown_solutions, _ = utils.solvers.newton_solver(constrained_residual, initial_values, - verbose=verbose, tol=tol, **solver_kwargs) + unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, + verbose=verbose, tol=tol, **solver_kwargs) unknown_solutions = list(unknown_solutions) elif solver == "solved": # If the entire solution is provided by the helper blocks @@ -281,123 +223,3 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, raise RuntimeError(f"steady_state is not yet compatible with {solver}.") return unknown_solutions - - -def extract_univariate_initial_values_or_bounds(unknowns): - val = next(iter(unknowns.values())) - if np.isscalar(val): - return {"x0": val} - else: - return {"bracket": (val[0], val[1])} - - -def extract_multivariate_initial_values_and_bounds(unknowns): - """Provided a dict mapping names of unknowns to initial values/bounds, return separate dicts of - the initial values and bounds. - Note: For one-sided bounds, simply put np.inf/-np.inf as the other side of the bounds, so there is - no ambiguity about which is the unconstrained side. -""" - initial_values = [] - multi_bounds = {} - for k, v in unknowns.items(): - if np.isscalar(v): - initial_values.append(v) - elif len(v) == 3: - lb, iv, ub = v - assert lb < iv < ub - initial_values.append(iv) - multi_bounds[k] = (lb, ub) - else: - raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." - f" the values of `unknowns` must either be a scalar, pertaining to a" - f" single initial value for the root solver to begin from," - f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") - - return np.asarray(initial_values), multi_bounds - - -def residual_with_linear_continuation(residual, bounds, eval_at_boundary=False, - boundary_epsilon=1e-4, penalty_scale=1e1, - verbose=False): - """Modify a residual function to implement bounds by an additive penalty for exceeding the boundaries - provided, scaled by the amount the guess exceeds the boundary. - - e.g. For residual function f(x), desiring x in (0, 1) (so assuming eval_at_boundary = False) - If the guess for x is 1.1 then we will censor to x_censored = 1 - boundary_epsilon, and return - f(x_censored) + penalty (where the penalty does not require re-evaluating f() which may be costly) - - residual: `function` - The function whose roots we want to solve for - bounds: `dict` - A dict mapping the names of the unknowns (`str`) to length two tuples corresponding to the lower and upper - bounds. - eval_at_boundary: `bool` - Whether to allow the residual function to be evaluated at exactly the boundary values or not. - Think of it as whether the solver will treat the bounds as creating a closed or open set for the search space. - boundary_epsilon: `float` - The amount to adjust the proposed guess, x, by to calculate the censored value of the residual function, - when the proposed guess exceeds the boundaries. - penalty_scale: `float` - The linear scaling factor for adjusting the penalty for the proposed unknown values exceeding the boundary. - verbose: `bool` - Whether to print out additional information for how the constrained residual function is behaving during - optimization. Useful for tuning the solver. - """ - lbs = np.asarray([v[0] for v in bounds.values()]) - ubs = np.asarray([v[1] for v in bounds.values()]) - - def constr_residual(x, residual_cache=[]): - """Implements a constrained residual function, where any attempts to evaluate x outside of the - bounds provided will result in a linear penalty function scaled by `penalty_scale`. - - Note: We are purposefully using residual_cache as a mutable default argument to cache the most recent - valid evaluation (maintain state between function calls) of the residual function to induce solvers - to backstep if they encounter a region of the search space that returns nan values. - See Hitchhiker's Guide to Python post on Mutable Default Arguments: "When the Gotcha Isn't a Gotcha" - """ - if eval_at_boundary: - x_censored = np.where(x < lbs, lbs, x) - x_censored = np.where(x > ubs, ubs, x_censored) - else: - x_censored = np.where(x < lbs, lbs + boundary_epsilon, x) - x_censored = np.where(x > ubs, ubs - boundary_epsilon, x_censored) - - residual_censored = residual(x_censored) - - if verbose: - print(f"Attempted x is {x}") - print(f"Censored x is {x_censored}") - print(f"The residual_censored is {residual_censored}") - - if np.any(np.isnan(residual_censored)): - # Provide a scaled penalty to the solver when trying to evaluate residual() in an undefined region - residual_censored = residual_cache[0] * penalty_scale - - if verbose: - print(f"The new residual_censored is {residual_censored}") - else: - if not residual_cache: - residual_cache.append(residual_censored) - else: - residual_cache[0] = residual_censored - - if verbose: - print(f"The residual_cache is {residual_cache[0]}") - - # Provide an additive, scaled penalty to the solver when trying to evaluate residual() outside of the boundary - residual_with_boundary_penalty = residual_censored + \ - (x - x_censored) * penalty_scale * residual_censored - return residual_with_boundary_penalty - - return constr_residual - - -def constrained_multivariate_residual(residual, bounds, method="linear_continuation", verbose=False, - **constrained_kwargs): - """Return a constrained version of the residual function, which accounts for bounds, using the specified method. - See the docstring of the specific method of interest for further details.""" - if method == "linear_continuation": - return residual_with_linear_continuation(residual, bounds, verbose=verbose, **constrained_kwargs) - # TODO: Implement logistic transform as another option for constrained multivariate residual - else: - raise ValueError(f"Method {method} for constrained multivariate root-finding has not yet been implemented.") diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py new file mode 100644 index 0000000..33ccb61 --- /dev/null +++ b/sequence_jacobian/steady_state/support.py @@ -0,0 +1,182 @@ +"""Various lower-level functions to support the computation of steady states""" + +from numbers import Real +import numpy as np + + +def provide_solver_default(unknowns): + if len(unknowns) == 1: + bounds = list(unknowns.values())[0] + if not isinstance(bounds, tuple) or bounds[0] > bounds[1]: + raise ValueError("Unable to find a compatible one-dimensional solver with provided `unknowns`.\n" + " Please provide valid lower/upper bounds, e.g. unknowns = {`a`: (0, 1)}") + else: + return "brentq" + elif len(unknowns) > 1: + init_values = list(unknowns.values()) + if not np.all([isinstance(v, Real) for v in init_values]): + raise ValueError("Unable to find a compatible multi-dimensional solver with provided `unknowns`.\n" + " Please provide valid initial values, e.g. unknowns = {`a`: 1, `b`: 2}") + else: + return "broyden_custom" + else: + raise ValueError("`unknowns` is empty! Please provide a dict of keys/values equal to the number of unknowns" + " that need to be solved for.") + + +def find_target_block(blocks, target): + for block in blocks: + if target in blocks.output: + return block + + +# Allow targets to be specified in the following formats +# 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) +# 2) target = {"r": 0.01} (allowing for the target to be non-zero) +# 3) target = {"K": "A"} (allowing the target to be another variable in potential_args) +def compute_target_values(targets, potential_args): + """ + For a given set of target specifications and potential arguments available, compute the targets. + Called as the return value for the residual function when utilizing the numerical solver. + + targets: Refer to `steady_state` function docstring + potential_args: Refer to the `steady_state` function docstring for the "calibration" variable + + return: A `float` (if computing a univariate target) or an `np.ndarray` (if using a multivariate target) + """ + target_values = np.empty(len(targets)) + for (i, t) in enumerate(targets): + v = targets[t] if isinstance(targets, dict) else 0 + if type(v) == str: + target_values[i] = potential_args[t] - potential_args[v] + else: + target_values[i] = potential_args[t] - v + # TODO: Implement feature to allow for an arbitrary explicit function expression as a potential target value + # e.g. targets = {"goods_mkt": "Y - C - I"}, so long as the expression is only comprise of generic numerical + # operators and variables solved for along the DAG prior to reaching the target. + + # Univariate solvers require float return values (and not lists) + if len(targets) == 1: + return target_values[0] + else: + return target_values + + +def extract_univariate_initial_values_or_bounds(unknowns): + val = next(iter(unknowns.values())) + if np.isscalar(val): + return {"x0": val} + else: + return {"bracket": (val[0], val[1])} + + +def extract_multivariate_initial_values_and_bounds(unknowns): + """Provided a dict mapping names of unknowns to initial values/bounds, return separate dicts of + the initial values and bounds. + Note: For one-sided bounds, simply put np.inf/-np.inf as the other side of the bounds, so there is + no ambiguity about which is the unconstrained side. +""" + initial_values = [] + multi_bounds = {} + for k, v in unknowns.items(): + if np.isscalar(v): + initial_values.append(v) + elif len(v) == 3: + lb, iv, ub = v + assert lb < iv < ub + initial_values.append(iv) + multi_bounds[k] = (lb, ub) + else: + raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." + f" the values of `unknowns` must either be a scalar, pertaining to a" + f" single initial value for the root solver to begin from," + f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") + + return np.asarray(initial_values), multi_bounds + + +def residual_with_linear_continuation(residual, bounds, eval_at_boundary=False, + boundary_epsilon=1e-4, penalty_scale=1e1, + verbose=False): + """Modify a residual function to implement bounds by an additive penalty for exceeding the boundaries + provided, scaled by the amount the guess exceeds the boundary. + + e.g. For residual function f(x), desiring x in (0, 1) (so assuming eval_at_boundary = False) + If the guess for x is 1.1 then we will censor to x_censored = 1 - boundary_epsilon, and return + f(x_censored) + penalty (where the penalty does not require re-evaluating f() which may be costly) + + residual: `function` + The function whose roots we want to solve for + bounds: `dict` + A dict mapping the names of the unknowns (`str`) to length two tuples corresponding to the lower and upper + bounds. + eval_at_boundary: `bool` + Whether to allow the residual function to be evaluated at exactly the boundary values or not. + Think of it as whether the solver will treat the bounds as creating a closed or open set for the search space. + boundary_epsilon: `float` + The amount to adjust the proposed guess, x, by to calculate the censored value of the residual function, + when the proposed guess exceeds the boundaries. + penalty_scale: `float` + The linear scaling factor for adjusting the penalty for the proposed unknown values exceeding the boundary. + verbose: `bool` + Whether to print out additional information for how the constrained residual function is behaving during + optimization. Useful for tuning the solver. + """ + lbs = np.asarray([v[0] for v in bounds.values()]) + ubs = np.asarray([v[1] for v in bounds.values()]) + + def constr_residual(x, residual_cache=[]): + """Implements a constrained residual function, where any attempts to evaluate x outside of the + bounds provided will result in a linear penalty function scaled by `penalty_scale`. + + Note: We are purposefully using residual_cache as a mutable default argument to cache the most recent + valid evaluation (maintain state between function calls) of the residual function to induce solvers + to backstep if they encounter a region of the search space that returns nan values. + See Hitchhiker's Guide to Python post on Mutable Default Arguments: "When the Gotcha Isn't a Gotcha" + """ + if eval_at_boundary: + x_censored = np.where(x < lbs, lbs, x) + x_censored = np.where(x > ubs, ubs, x_censored) + else: + x_censored = np.where(x < lbs, lbs + boundary_epsilon, x) + x_censored = np.where(x > ubs, ubs - boundary_epsilon, x_censored) + + residual_censored = residual(x_censored) + + if verbose: + print(f"Attempted x is {x}") + print(f"Censored x is {x_censored}") + print(f"The residual_censored is {residual_censored}") + + if np.any(np.isnan(residual_censored)): + # Provide a scaled penalty to the solver when trying to evaluate residual() in an undefined region + residual_censored = residual_cache[0] * penalty_scale + + if verbose: + print(f"The new residual_censored is {residual_censored}") + else: + if not residual_cache: + residual_cache.append(residual_censored) + else: + residual_cache[0] = residual_censored + + if verbose: + print(f"The residual_cache is {residual_cache[0]}") + + # Provide an additive, scaled penalty to the solver when trying to evaluate residual() outside of the boundary + residual_with_boundary_penalty = residual_censored + \ + (x - x_censored) * penalty_scale * residual_censored + return residual_with_boundary_penalty + + return constr_residual + + +def constrained_multivariate_residual(residual, bounds, method="linear_continuation", verbose=False, + **constrained_kwargs): + """Return a constrained version of the residual function, which accounts for bounds, using the specified method. + See the docstring of the specific method of interest for further details.""" + if method == "linear_continuation": + return residual_with_linear_continuation(residual, bounds, verbose=verbose, **constrained_kwargs) + # TODO: Implement logistic transform as another option for constrained multivariate residual + else: + raise ValueError(f"Method {method} for constrained multivariate root-finding has not yet been implemented.") From c0cfd54ef61fe6f4a082c28acf8b4765e36a71b2 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 17:10:01 -0600 Subject: [PATCH 042/288] Update skeleton of Block abstract base class, WIP. Not yet functional --- sequence_jacobian/base.py | 6 ++- sequence_jacobian/primitives.py | 91 +++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/sequence_jacobian/base.py b/sequence_jacobian/base.py index ed6796f..ff76d43 100644 --- a/sequence_jacobian/base.py +++ b/sequence_jacobian/base.py @@ -1,13 +1,15 @@ """Type aliases, custom functions and errors for the base-level functionality of the package""" -from .primitives import Block +from typing import Any + from .blocks.combined_block import CombinedBlock, combine +# Basic types +Array = Any # Useful type aliases Model = CombinedBlock - # Useful functional aliases def create_model(*args, **kwargs): return combine(*args, model_alias=True, **kwargs) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 0fe3787..7fb8b02 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -1,50 +1,85 @@ """Primitives to provide clarity and structure on blocks/models work""" -import numpy as np import abc from numbers import Real -from typing import Dict, Union, List +from typing import Dict, Union, Tuple, Optional, List -from . import utilities as utils -from .base import BlockArray +from .base import Array +from .jacobian.classes import JacobianDict +from .steady_state.drivers import steady_state +from .nonlinear import td_solve +from .jacobian.drivers import get_G -# TODO: Refactor .ss, .td, and .jac methods for SimpleBlock and HetBlock to be cleaner so they can be interpreted from -# this more abstract representation of what canonical "Block"-like behavior should be -class Block(object): - __metaclass__ = abc.ABCMeta +class Block(abc.ABC): + """The abstract base class for all `Block` objects.""" @abc.abstractmethod - def __init__(self, f): - self.f = f - self.inputs = set(utils.misc.input_list(f)) - self.outputs = set(utils.misc.output_list(f)) + def __init__(self): + pass + @property @abc.abstractmethod - def ss(self, *ss_args, **ss_kwargs) -> Dict[str, Union[Real, np.ndarray]]: - """Call the block's function attribute `.f` on the pre-processed steady state (keyword) arguments, - ensuring that any time displacements will be ignored when `.f` is called. - See blocks.support.simple_displacement for an example of how SimpleBlocks do this pre-processing.""" - return self.f(*ss_args, **ss_kwargs) + def inputs(self): + pass + + @property + @abc.abstractmethod + def outputs(self): + pass + # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical + # input and output argument structure @abc.abstractmethod - def td(self, ss: Dict[str, Real], shock_paths: Dict[str, np.ndarray], **kwargs) -> Dict[str, np.ndarray]: + def steady_state(self, *ss_args, **ss_kwargs) -> Dict[str, Union[Real, Array]]: pass @abc.abstractmethod - def jac(self, ss, shock_list, T): + def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], + shocked_paths: Dict[str, Array], **kwargs) -> Dict[str, Array]: pass + @abc.abstractmethod + def impulse_linear(self, ss: Dict[str, Union[Real, Array]], + shocked_paths: Dict[str, Array], **kwargs) -> Dict[str, Array]: + pass -class Model(object): - __metaclass__ = abc.ABCMeta + @abc.abstractmethod + def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous=None, T=None, **kwargs) -> JacobianDict: + pass @abc.abstractmethod - def __init__(self, blocks: BlockArray, exogenous: List[str], unknowns: List[str], targets: List[str]) -> None: - self.blocks = blocks - self.exogenous = exogenous - self.unknowns = unknowns - self.targets = targets + def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], + unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], + targets: Union[Array, Dict[str, Union[str, Real]]], + solver: Optional[str] = "", **kwargs) -> Dict[str, Union[Real, Array]]: + # What is a consistent interface for passing things to steady_state? + # Should change steady_state from expecting a block_list to a *single* Block object + # duck-type by checking for attr ".blocks", to signify if is a CombinedBlock + # Should try to figure out a nicer way to pass variable kwargs to eval_block_ss to clean that function up - # TODO: Implement standard checks, as in CombinedBlock in SHADE, for cyclic dependence, the right number of - # unknowns and targets etc. to ensure that the model is well-defined. + # Also should change td_solve and get_G to also only expect *single* Block objects, with a deprecation + # allowing for lists to be passed, which will then automatically build those lists into CombinedBlocks + pass + + @abc.abstractmethod + def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], + unknowns: List[str], targets: List[str], + in_deviations: Optional[bool] = True, **kwargs) -> Dict[str, Array]: + pass + + @abc.abstractmethod + def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], + unknowns: List[str], targets: List[str], + T: Optional[int] = None, in_deviations: Optional[bool] = True, + **kwargs) -> Dict[str, Array]: + pass + + @abc.abstractmethod + def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], + exogenous: List[str], + unknowns: List[str], targets: List[str], + T: Optional[int] = None, **kwargs) -> Dict[str, Array]: + pass From 1c796fd5e0320e8a515ea9056b3e8dfef5884a05 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 17:16:40 -0600 Subject: [PATCH 043/288] Add additional accuracy specifications for steady_state --- sequence_jacobian/steady_state/drivers.py | 26 +++++++++++++++++------ sequence_jacobian/steady_state/support.py | 15 +++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 1760fa8..a723b9a 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -5,7 +5,7 @@ from copy import deepcopy from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ - extract_univariate_initial_values_or_bounds, constrained_multivariate_residual + extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check from ..utilities import solvers, graph from ..utilities.misc import unprime, dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs, make_tuple from ..blocks.simple_block import SimpleBlock @@ -15,8 +15,9 @@ # Find the steady state solution def steady_state(blocks, calibration, unknowns, targets, - consistency_check=True, ttol=1e-9, ctol=1e-9, - verbose=False, solver=None, solver_kwargs=None, + consistency_check=True, ttol=2e-12, ctol=1e-9, + backward_tol=1e-8, forward_tol=1e-10, + verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. @@ -37,6 +38,12 @@ def steady_state(blocks, calibration, unknowns, targets, ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the use of HelperBlocks, to equal the desired values + backward_tol/forward_tol: `float` + See `HetBlock` .ss method docstring for details on these additional convergence tolerances + verbose: `bool` + Display the content of optional print statements within the solver for more responsive feedback + fragile: `bool` + Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails solver: `string` The name of the numerical solver that the user would like to user. Can either be a custom solver the user implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root @@ -74,7 +81,8 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False continue else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose) + ttol=ttol, ctol=ctol, backward_tol=backward_tol, + forward_tol=forward_tol, verbose=verbose) if include_helpers and isinstance(blocks[i], HelperBlock): helper_outputs.update(outputs) ss_values.update(outputs) @@ -100,7 +108,8 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check: - assert abs(np.max(residual(unknown_solutions, include_helpers=False))) < ctol + cresid = abs(np.max(residual(unknown_solutions, include_helpers=False))) + run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns ss_values.update(zip(unknowns, make_tuple(unknown_solutions))) @@ -113,7 +122,8 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. -def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False, **kwargs): +def eval_block_ss(block, potential_args, consistency_check=True, ttol=2e-12, ctol=1e-9, + backward_tol=1e-8, forward_tol=1e-10, verbose=False, **kwargs): """ Evaluate the .ss method of a block, given a dictionary of potential arguments. @@ -125,8 +135,10 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=1e-9, ctol # Simple and HetBlocks require different handling of block.ss() output since # SimpleBlocks return a tuple of un-labeled arguments, whereas HetBlocks return dictionaries - if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock) or isinstance(block, HetBlock): + if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock): outputs = block.steady_state(input_args, **kwargs) + elif isinstance(block, HetBlock): + outputs = block.steady_state(input_args, backward_tol=backward_tol, forward_tol=forward_tol, **kwargs) else: # since .ss for SolvedBlocks calls the steady_state driver function outputs = block.steady_state(input_args, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index 33ccb61..bcb6893 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -1,5 +1,6 @@ """Various lower-level functions to support the computation of steady states""" +import warnings from numbers import Real import numpy as np @@ -24,6 +25,20 @@ def provide_solver_default(unknowns): " that need to be solved for.") +def run_consistency_check(cresid, ctol=1e-9, fragile=False): + if cresid > ctol: + if fragile: + raise RuntimeError(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + else: + warnings.warn(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + + def find_target_block(blocks, target): for block in blocks: if target in blocks.output: From 3b899b61df6cd3f9b9be3dbdf9eafc21ac12f8f6 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 17:25:00 -0600 Subject: [PATCH 044/288] Add idiosyncratic_variables attribute to SteadyStateDict --- sequence_jacobian/steady_state/classes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index d695b31..db95c47 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -39,3 +39,6 @@ def __setitem__(self, key, value): def aggregates(self): return {k: v for k, v in self.data.items() if np.isscalar(v)} + + def idiosyncratic_variables(self): + return {k: v for k, v in self.data.items() if not np.isscalar(v)} From 69f975f44c1d3392971f61a119cd0549c398060a Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 21:34:40 -0600 Subject: [PATCH 045/288] Add enhanced input_list convenience functions --- sequence_jacobian/utilities/misc.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 6a693a3..9f585ef 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -17,8 +17,26 @@ def make_tuple(x): def input_list(f): - """Return list of function inputs""" - return inspect.getfullargspec(f).args + """Return list of function inputs (both positional and keyword arguments)""" + return list(inspect.signature(f).parameters) + + +def input_arg_list(f): + """Return list of function positional arguments *only*""" + arg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default == p.empty: + arg_list.append(p.name) + return arg_list + + +def input_kwarg_list(f): + """Return list of function keyword arguments *only*""" + kwarg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default != p.empty: + kwarg_list.append(p.name) + return kwarg_list def output_list(f): From 4400e44e296a056f766a280faf8d1b371c5723a8 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 21:35:16 -0600 Subject: [PATCH 046/288] Simplify eval_block_ss to get rid of explicit Block type checking --- sequence_jacobian/steady_state/drivers.py | 50 ++++++++--------------- sequence_jacobian/steady_state/support.py | 6 --- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index a723b9a..7d546d0 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -7,17 +7,15 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check from ..utilities import solvers, graph -from ..utilities.misc import unprime, dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs, make_tuple -from ..blocks.simple_block import SimpleBlock +from ..utilities.misc import dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs,\ + make_tuple, input_kwarg_list from ..blocks.helper_block import HelperBlock -from ..blocks.het_block import HetBlock # Find the steady state solution def steady_state(blocks, calibration, unknowns, targets, consistency_check=True, ttol=2e-12, ctol=1e-9, - backward_tol=1e-8, forward_tol=1e-10, - verbose=False, fragile=False, solver=None, solver_kwargs=None, + block_kwargs=None, verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. @@ -38,8 +36,10 @@ def steady_state(blocks, calibration, unknowns, targets, ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the use of HelperBlocks, to equal the desired values - backward_tol/forward_tol: `float` - See `HetBlock` .ss method docstring for details on these additional convergence tolerances + block_kwargs: `dict` + A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any + potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that + Block sub-class. verbose: `bool` Display the content of optional print statements within the solver for more responsive feedback fragile: `bool` @@ -61,6 +61,8 @@ def steady_state(blocks, calibration, unknowns, targets, """ # Populate otherwise mutable default arguments + if block_kwargs is None: + block_kwargs = {} if solver_kwargs is None: solver_kwargs = {} if constrained_kwargs is None: @@ -81,8 +83,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False continue else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, backward_tol=backward_tol, - forward_tol=forward_tol, verbose=verbose) + ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) if include_helpers and isinstance(blocks[i], HelperBlock): helper_outputs.update(outputs) ss_values.update(outputs) @@ -116,39 +117,20 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Find the hetoutputs of the Hetblocks that have hetoutputs for i in find_blocks_with_hetoutputs(blocks): - ss_values.update(eval_block_ss(blocks[i], ss_values, hetoutput=True)) + ss_values.update(eval_block_ss(blocks[i], ss_values, hetoutput=True, **block_kwargs)) return ss_values -# Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. -def eval_block_ss(block, potential_args, consistency_check=True, ttol=2e-12, ctol=1e-9, - backward_tol=1e-8, forward_tol=1e-10, verbose=False, **kwargs): - """ - Evaluate the .ss method of a block, given a dictionary of potential arguments. - - Refer to the `steady_state` function docstring for information on args/kwargs - - return: A `dict` of output names (as `str`) and output values from evaluating the .ss method of a block - """ - input_args = {unprime(arg_name): potential_args[unprime(arg_name)] for arg_name in block.inputs} - - # Simple and HetBlocks require different handling of block.ss() output since - # SimpleBlocks return a tuple of un-labeled arguments, whereas HetBlocks return dictionaries - if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock): - outputs = block.steady_state(input_args, **kwargs) - elif isinstance(block, HetBlock): - outputs = block.steady_state(input_args, backward_tol=backward_tol, forward_tol=forward_tol, **kwargs) - else: # since .ss for SolvedBlocks calls the steady_state driver function - outputs = block.steady_state(input_args, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) - - return outputs +def eval_block_ss(block, calibration, **kwargs): + """Evaluate the .ss method of a block, given a dictionary of potential arguments""" + return block.steady_state({k: v for k, v in calibration.items() if k in block.inputs}, + **{k: v for k, v in kwargs.items() if k in input_kwarg_list(block.steady_state)}) def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, constrained_method, constrained_kwargs, - tol=1e-9, verbose=False): + tol=2e-12, verbose=False): """ Given a residual function (constructed within steady_state) and a set of bounds or initial values for the set of unknowns, solve for the root. diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index bcb6893..940dff2 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -39,12 +39,6 @@ def run_consistency_check(cresid, ctol=1e-9, fragile=False): f" If this is not an issue, adjust ctol accordingly.") -def find_target_block(blocks, target): - for block in blocks: - if target in blocks.output: - return block - - # Allow targets to be specified in the following formats # 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) # 2) target = {"r": 0.01} (allowing for the target to be non-zero) From 9c1557d1eec1527646f505f2b777f1985c1acca8 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 23 Feb 2021 21:54:56 -0600 Subject: [PATCH 047/288] Grab code from StackExchange that implements "abstract attributes", which aren't in abc --- sequence_jacobian/__init__.py | 2 +- sequence_jacobian/{base.py => aliases.py} | 5 -- sequence_jacobian/primitives.py | 60 ++++++++++++++++------- 3 files changed, 44 insertions(+), 23 deletions(-) rename sequence_jacobian/{base.py => aliases.py} (86%) diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 08e79c6..eeaa0a7 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -4,7 +4,7 @@ from .models import rbc, krusell_smith, hank, two_asset -from .base import create_model +from .aliases import create_model from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput diff --git a/sequence_jacobian/base.py b/sequence_jacobian/aliases.py similarity index 86% rename from sequence_jacobian/base.py rename to sequence_jacobian/aliases.py index ff76d43..aedcfe6 100644 --- a/sequence_jacobian/base.py +++ b/sequence_jacobian/aliases.py @@ -1,12 +1,7 @@ """Type aliases, custom functions and errors for the base-level functionality of the package""" -from typing import Any - from .blocks.combined_block import CombinedBlock, combine -# Basic types -Array = Any - # Useful type aliases Model = CombinedBlock diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 7fb8b02..2f135b0 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -1,30 +1,63 @@ """Primitives to provide clarity and structure on blocks/models work""" import abc +from abc import ABCMeta as NativeABCMeta from numbers import Real -from typing import Dict, Union, Tuple, Optional, List +from typing import Any, Dict, Union, Tuple, Optional, List -from .base import Array from .jacobian.classes import JacobianDict -from .steady_state.drivers import steady_state -from .nonlinear import td_solve -from .jacobian.drivers import get_G +# Basic types +Array = Any -class Block(abc.ABC): + +############################################################################### +# Because abc doesn't implement "abstract attribute"s +# https://stackoverflow.com/questions/23831510/abstract-attribute-not-property +class DummyAttribute: + pass + + +def abstract_attribute(obj=None): + if obj is None: + obj = DummyAttribute() + obj.__is_abstract_attribute__ = True + return obj + + +class ABCMeta(NativeABCMeta): + + def __call__(cls, *args, **kwargs): + instance = NativeABCMeta.__call__(cls, *args, **kwargs) + abstract_attributes = { + name + for name in dir(instance) + if getattr(getattr(instance, name), '__is_abstract_attribute__', False) + } + if abstract_attributes: + raise NotImplementedError( + "Can't instantiate abstract class {} with" + " abstract attributes: {}".format( + cls.__name__, + ', '.join(abstract_attributes) + ) + ) + return instance +############################################################################### + + +class Block(abc.ABC, metaclass=ABCMeta): """The abstract base class for all `Block` objects.""" @abc.abstractmethod def __init__(self): pass - @property - @abc.abstractmethod + @abstract_attribute def inputs(self): pass - @property - @abc.abstractmethod + @abstract_attribute def outputs(self): pass @@ -53,13 +86,6 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], solver: Optional[str] = "", **kwargs) -> Dict[str, Union[Real, Array]]: - # What is a consistent interface for passing things to steady_state? - # Should change steady_state from expecting a block_list to a *single* Block object - # duck-type by checking for attr ".blocks", to signify if is a CombinedBlock - # Should try to figure out a nicer way to pass variable kwargs to eval_block_ss to clean that function up - - # Also should change td_solve and get_G to also only expect *single* Block objects, with a deprecation - # allowing for lists to be passed, which will then automatically build those lists into CombinedBlocks pass @abc.abstractmethod From e52fde914fa70669a70fae26ae45922ecd746f9d Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 24 Feb 2021 10:41:28 -0600 Subject: [PATCH 048/288] Implement Block inheritance for SimpleBlock and CombinedBlock WIP --- sequence_jacobian/blocks/combined_block.py | 9 +++++---- sequence_jacobian/blocks/simple_block.py | 17 ++++++++++++++++- sequence_jacobian/primitives.py | 8 +++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 357de3d..5bf725f 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -3,8 +3,9 @@ from copy import deepcopy import numpy as np +from ..primitives import Block from .. import utilities as utils -from ..steady_state.drivers import eval_block_ss, steady_state +from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..nonlinear import td_solve from ..jacobian.drivers import get_G @@ -17,7 +18,7 @@ def combine(*args, name="", model_alias=False): return CombinedBlock(*args, name=name, model_alias=model_alias) -class CombinedBlock: +class CombinedBlock(Block): """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, and calculates Jacobians along the DAG""" @@ -144,8 +145,8 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar if solver is None: solver = provide_solver_default(unknowns) - ss_gen_eq = steady_state(self.blocks_w_helpers, calibration, unknowns, targets, solver=solver, **kwargs) - return ss_gen_eq + + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index a8c45b6..9485f19 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -2,6 +2,7 @@ import numpy as np from .support.simple_displacement import ignore, Displace, AccumulatedDerivative +from ..primitives import Block from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc @@ -14,7 +15,7 @@ def simple(f): return SimpleBlock(f) -class SimpleBlock: +class SimpleBlock(Block): """Generated from simple block written in Dynare-ish style and decorated with @simple, e.g. @simple @@ -169,6 +170,20 @@ def jacobian(self, ss, exogenous=None, T=None): return JacobianDict(J) + def solve_steady_state(self, calibration, unknowns, targets, solver="", **kwargs): + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) + + def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, + in_deviations=True, **kwargs): + pass + + def solve_impulse_linear(self, ss, exogenous, unknowns, targets, + T=None, in_deviations=True, **kwargs): + pass + + def solve_jacobian(self, ss, exogenous, unknowns, targets, T=None, **kwargs): + pass + def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 2f135b0..c10135f 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -5,6 +5,9 @@ from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List +from .steady_state.drivers import steady_state +from .nonlinear import td_solve +from .jacobian.drivers import get_G from .jacobian.classes import JacobianDict # Basic types @@ -86,7 +89,10 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], solver: Optional[str] = "", **kwargs) -> Dict[str, Union[Real, Array]]: - pass + if hasattr(self, "blocks_w_helpers"): + return steady_state(self.blocks_w_helpers, calibration, unknowns, targets, solver=solver, **kwargs) + else: + return steady_state([self], calibration, unknowns, targets, solver=solver, **kwargs) @abc.abstractmethod def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], From 0972ddb4c271ad16e9fbd1f9839809a86d78184d Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 1 Mar 2021 12:36:03 -0600 Subject: [PATCH 049/288] Resolving circular imports and partially refactor to setup Block WIP --- sequence_jacobian/blocks/combined_block.py | 2 +- sequence_jacobian/jacobian/drivers.py | 9 +++---- sequence_jacobian/nonlinear.py | 29 +++++++++------------- sequence_jacobian/primitives.py | 3 ++- sequence_jacobian/steady_state/drivers.py | 18 ++++++-------- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 5bf725f..8c0f03d 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -152,7 +152,7 @@ def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviation """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - irf_nonlin_gen_eq = td_solve(ss, self.blocks, unknowns, targets, + irf_nonlin_gen_eq = td_solve(self.blocks, ss, unknowns, targets, exogenous={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) # Default to percentage deviations from steady state. If the steady state value is zero, then just return diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 0d10e23..8718698 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -5,7 +5,6 @@ from .classes import JacobianDict from .support import pack_vectors, unpack_vectors from ..utilities import misc, graph -from ..blocks.simple_block import SimpleBlock '''Drivers: - get_H_U : get H_U matrix mapping all unknowns to all targets @@ -213,11 +212,9 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=Fal for num in topsorted: block = block_list[num] - if hasattr(block, 'jac'): - if isinstance(block, SimpleBlock): - jac = block.jac(ss, shock_list=list(shocks)) - else: - jac = block.jac(ss, shock_list=list(shocks), T=T, save=save, use_saved=use_saved) + if hasattr(block, 'jacobian'): + jac = block.jacobian(ss, shock_list=list(shocks), **{k: v for k, v in {"T": T, "save": save, "use_saved": use_saved} + if k in misc.input_kwarg_list(block.jacobian)}) else: # doesn't have 'jac', must be nested dict that is jac directly jac = block diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 9970079..c738ab0 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -2,15 +2,14 @@ import numpy as np -from . import utilities as utils -from .blocks import het_block as het +from .utilities import misc, graph from .jacobian.drivers import get_H_U from .jacobian.support import pack_vectors, unpack_vectors from .devtools.deprecate import deprecated_shock_input_convention -def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_factored=None, monotonic=False, +def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_factored=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, save=False, use_saved=False, grid_paths=None, **kwargs): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. @@ -19,8 +18,8 @@ def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_fa Parameters ---------- - ss : dict, all steady-state information block_list : list, blocks in model (SimpleBlocks or HetBlocks) + ss : dict, all steady-state information unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) exogenous : dict, all shocked Z go here, must all have same length T @@ -60,14 +59,14 @@ def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_fa if H_U is None: # not even H_U is supplied, get it (costly if there are HetBlocks) H_U = get_H_U(block_list, unknowns, targets, T, ss, save=save, use_saved=use_saved) - H_U_factored = utils.misc.factor(H_U) + H_U_factored = misc.factor(H_U) # do a topological sort once to avoid some redundancy - sort = utils.graph.block_sort(block_list, ignore_helpers=True) + sort = graph.block_sort(block_list, ignore_helpers=True) # iterate until convergence for it in range(maxit): - results = td_map(ss, block_list, sort=sort, monotonic=monotonic, returnindividual=returnindividual, + results = td_map(block_list, ss, sort=sort, monotonic=monotonic, returnindividual=returnindividual, grid_paths=grid_paths, **exogenous, **Us) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: @@ -79,7 +78,7 @@ def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_fa else: # update guess U by -H_U^(-1) times errors H Hvec = pack_vectors(results, targets, T) - Uvec -= utils.misc.factored_solve(H_U_factored, Hvec) + Uvec -= misc.factored_solve(H_U_factored, Hvec) Us = unpack_vectors(Uvec, unknowns, T) else: raise ValueError(f'No convergence after {maxit} backward iterations!') @@ -87,7 +86,7 @@ def td_solve(ss, block_list, unknowns, targets, exogenous=None, H_U=None, H_U_fa return results -def td_map(ss, block_list, exogenous=None, sort=None, monotonic=False, returnindividual=False, +def td_map(block_list, ss, exogenous=None, sort=None, monotonic=False, returnindividual=False, grid_paths=None, **kwargs): """Helper for td_solve, calculates H(U, Z), where U and Z are in kwargs. @@ -99,13 +98,8 @@ def td_map(ss, block_list, exogenous=None, sort=None, monotonic=False, returnind # first get topological sort if none already provided if sort is None: - sort = utils.graph.block_sort(block_list, ignore_helpers=True) + sort = graph.block_sort(block_list, ignore_helpers=True) - # TODO: Seems a bit weird that you pass variables ad hoc from the top-level - # e.g. calling nonlinear.td_solve() with rstar as a kwarg in one asset HANK. - # look more into why this is done and if it could be done better. - # TODO: Rename the various references to kwargs/results to be more informative - # if we do end up keeping this top-level functionality for passing in variables # initialize results results = exogenous for n in sort: @@ -116,8 +110,9 @@ def td_map(ss, block_list, exogenous=None, sort=None, monotonic=False, returnind raise ValueError(f'Block {block} outputting already-present outputs {block.outputs & results.keys()}') # if any input to the block has changed, run the block - blockoptions = hetoptions if isinstance(block, het.HetBlock) else {} if not block.inputs.isdisjoint(results): - results.update(block.td(ss, **blockoptions, **{k: results[k] for k in block.inputs if k in results})) + results.update(block.impulse_nonlinear(ss, **{k: v for k, v in hetoptions.items() + if k in misc.input_kwarg_list(block.impulse_nonlinear)}, + **{k: results[k] for k in block.inputs if k in results})) return results diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index c10135f..388c182 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -84,7 +84,8 @@ def impulse_linear(self, ss: Dict[str, Union[Real, Array]], def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous=None, T=None, **kwargs) -> JacobianDict: pass - @abc.abstractmethod + # TODO: Implement this as a concrete method + # @abc.abstractmethod def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 7d546d0..2cfdc16 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -6,9 +6,7 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check -from ..utilities import solvers, graph -from ..utilities.misc import dict_diff, smart_zip, smart_zeros, find_blocks_with_hetoutputs,\ - make_tuple, input_kwarg_list +from ..utilities import solvers, graph, misc from ..blocks.helper_block import HelperBlock @@ -72,7 +70,7 @@ def steady_state(blocks, calibration, unknowns, targets, topsorted = graph.block_sort(blocks, calibration=calibration) def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False): - ss_values.update(smart_zip(unknowns.keys(), unknown_values)) + ss_values.update(misc.smart_zip(unknowns.keys(), unknown_values)) helper_outputs = {} @@ -90,14 +88,14 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False else: # Don't overwrite entries in ss_values corresponding to what has already # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(dict_diff(outputs, helper_outputs)) + ss_values.update(misc.dict_diff(outputs, helper_outputs)) # Update the "unknowns" dictionary *in place* with its steady state values. # i.e. the "unknowns" in the namespace in which this function is invoked will change! # Useful for a) if the unknown values are updated while iterating each blocks' ss computation within the DAG, # and/or b) if the user wants to update "unknowns" in place for use in other computations. if update_unknowns_inplace: - unknowns.update(smart_zip(unknowns.keys(), [ss_values[key] for key in unknowns.keys()])) + unknowns.update(misc.smart_zip(unknowns.keys(), [ss_values[key] for key in unknowns.keys()])) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" @@ -113,10 +111,10 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns - ss_values.update(zip(unknowns, make_tuple(unknown_solutions))) + ss_values.update(zip(unknowns, misc.make_tuple(unknown_solutions))) # Find the hetoutputs of the Hetblocks that have hetoutputs - for i in find_blocks_with_hetoutputs(blocks): + for i in misc.find_blocks_with_hetoutputs(blocks): ss_values.update(eval_block_ss(blocks[i], ss_values, hetoutput=True, **block_kwargs)) return ss_values @@ -125,7 +123,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False def eval_block_ss(block, calibration, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" return block.steady_state({k: v for k, v in calibration.items() if k in block.inputs}, - **{k: v for k, v in kwargs.items() if k in input_kwarg_list(block.steady_state)}) + **{k: v for k, v in kwargs.items() if k in misc.input_kwarg_list(block.steady_state)}) def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, @@ -211,7 +209,7 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, # Call residual() once to update ss_values and to check the targets match the provided solution. # The initial value passed into residual (np.zeros()) in this case is irrelevant, but something # must still be passed in since the residual function requires an argument. - assert abs(np.max(residual(smart_zeros(len(unknowns)), update_unknowns_inplace=True))) < tol + assert abs(np.max(residual(misc.smart_zeros(len(unknowns)), update_unknowns_inplace=True))) < tol unknown_solutions = list(unknowns.values()) else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") From f889f501074c10ddae1c57cfd92376bf65ac62ef Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 1 Mar 2021 12:38:27 -0600 Subject: [PATCH 050/288] Remove BlockArray dependencies in primitives --- sequence_jacobian/primitives.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 0fe3787..50e57d2 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -3,10 +3,9 @@ import numpy as np import abc from numbers import Real -from typing import Dict, Union, List +from typing import Dict, Union from . import utilities as utils -from .base import BlockArray # TODO: Refactor .ss, .td, and .jac methods for SimpleBlock and HetBlock to be cleaner so they can be interpreted from @@ -34,17 +33,3 @@ def td(self, ss: Dict[str, Real], shock_paths: Dict[str, np.ndarray], **kwargs) @abc.abstractmethod def jac(self, ss, shock_list, T): pass - - -class Model(object): - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __init__(self, blocks: BlockArray, exogenous: List[str], unknowns: List[str], targets: List[str]) -> None: - self.blocks = blocks - self.exogenous = exogenous - self.unknowns = unknowns - self.targets = targets - - # TODO: Implement standard checks, as in CombinedBlock in SHADE, for cyclic dependence, the right number of - # unknowns and targets etc. to ensure that the model is well-defined. From 4ce94221944583557c50cf23840ae3a894b6704c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Mar 2021 13:23:32 -0600 Subject: [PATCH 051/288] Add hetoutput as a top-level steady_state kwarg to compute hetoutputs at each iteration instead of at the end (useful if used as targets) --- sequence_jacobian/steady_state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/steady_state.py b/sequence_jacobian/steady_state.py index fa93eb9..5ad88a0 100644 --- a/sequence_jacobian/steady_state.py +++ b/sequence_jacobian/steady_state.py @@ -16,7 +16,7 @@ # Find the steady state solution def steady_state(blocks, calibration, unknowns, targets, consistency_check=True, ttol=2e-12, ctol=1e-9, - backward_tol=1e-8, forward_tol=1e-10, + backward_tol=1e-8, forward_tol=1e-10, hetoutput=False, verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ @@ -82,7 +82,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, backward_tol=backward_tol, - forward_tol=forward_tol, verbose=verbose) + forward_tol=forward_tol, hetoutput=hetoutput, verbose=verbose) if include_helpers and isinstance(blocks[i], HelperBlock): helper_outputs.update(outputs) ss_values.update(outputs) @@ -196,7 +196,7 @@ def compute_target_values(targets, potential_args): # Analogous to the SHADE workflow of having blocks call utils.apply(self._fss, inputs) but not as general. def eval_block_ss(block, potential_args, consistency_check=True, ttol=2e-12, ctol=1e-9, - backward_tol=1e-8, forward_tol=1e-10, verbose=False, **kwargs): + backward_tol=1e-8, forward_tol=1e-10, hetoutput=False, verbose=False, **kwargs): """ Evaluate the .ss method of a block, given a dictionary of potential arguments. @@ -211,7 +211,8 @@ def eval_block_ss(block, potential_args, consistency_check=True, ttol=2e-12, cto if isinstance(block, SimpleBlock) or isinstance(block, HelperBlock): outputs = block.steady_state(input_args, **kwargs) elif isinstance(block, HetBlock): - outputs = block.steady_state(input_args, backward_tol=backward_tol, forward_tol=forward_tol, **kwargs) + outputs = block.steady_state(input_args, backward_tol=backward_tol, forward_tol=forward_tol, + hetoutput=hetoutput, **kwargs) else: # since .ss for SolvedBlocks calls the steady_state driver function outputs = block.steady_state(input_args, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **kwargs) From 14d49c4413ab6d68712b62d2269fad33fb780937 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 3 Mar 2021 13:23:53 -0600 Subject: [PATCH 052/288] Ensure self.inputs for HetBlocks accounts for hetintput_outputs that are hetoutput_inputs --- sequence_jacobian/blocks/het_block.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 1210f9b..789a736 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -509,7 +509,7 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation # but is computed after the backward iteration - self.inputs |= (self.hetoutput_inputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) + self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) # Modify the HetBlock's outputs to include the aggregated hetoutputs self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) From 783c674fd64c8b615c1a2624c7e2fe811dd2b286 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 10:45:44 -0600 Subject: [PATCH 053/288] Enhance numeric_primitive to also convert tuples and lists of numerics into tuples and lists of their primitives --- sequence_jacobian/utilities/misc.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 9f585ef..d9fa868 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -54,8 +54,21 @@ def output_list(f): def numeric_primitive(instance): # If it is already a primitive, just return it - if type(instance) in {int, float, np.ndarray}: + if type(instance) in {int, float}: return instance + elif isinstance(instance, np.ndarray): + if np.issubdtype(instance.dtype, np.number): + return instance + else: + raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance.dtype}," + f" which is not a valid numeric type.") + elif type(instance) in {tuple, list}: + instance_array = np.asarray(instance) + if np.issubdtype(instance_array.dtype, np.number): + return type(instance)(instance_array) + else: + raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance_array.dtype}," + f" which is not a valid numeric type.") else: return instance.real if np.isscalar(instance) else instance.base From 48b760b165d972dc3250cf0373d0f4b9e282aa3b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 10:46:27 -0600 Subject: [PATCH 054/288] Change T positional to keyword arguments w/ default values for HetBlock jacobian and get_G for consistency across blocks --- sequence_jacobian/blocks/het_block.py | 2 +- sequence_jacobian/jacobian/drivers.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 1210f9b..10b6272 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -372,7 +372,7 @@ def impulse_linear(self, ss, exogenous, T=None, **kwargs): return self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous) - def jacobian(self, ss, exogenous, T, output_list=None, h=1E-4, save=False, use_saved=False): + def jacobian(self, ss, exogenous, T=300, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 8718698..f77084f 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -117,7 +117,7 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None return {o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs} -def get_G(block_list, exogenous, unknowns, targets, T, ss=None, outputs=None, +def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None, save=False, use_saved=False): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -213,8 +213,8 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=Fal block = block_list[num] if hasattr(block, 'jacobian'): - jac = block.jacobian(ss, shock_list=list(shocks), **{k: v for k, v in {"T": T, "save": save, "use_saved": use_saved} - if k in misc.input_kwarg_list(block.jacobian)}) + jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T, "save": save, "use_saved": use_saved}.items() + if k in misc.input_kwarg_list(block.jacobian)}) else: # doesn't have 'jac', must be nested dict that is jac directly jac = block From ad374dc338f2e3672a1bbd2ff62b6c49a7171e26 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 10:46:48 -0600 Subject: [PATCH 055/288] Remove SimpleBlock, HetBlock, and SolvedBlock dependencies from block_sort to avoid circular imports --- sequence_jacobian/utilities/graph.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sequence_jacobian/utilities/graph.py b/sequence_jacobian/utilities/graph.py index 66e3163..0ca62aa 100644 --- a/sequence_jacobian/utilities/graph.py +++ b/sequence_jacobian/utilities/graph.py @@ -1,9 +1,6 @@ """Topological sort and related code""" -from ..blocks.simple_block import SimpleBlock -from ..blocks.het_block import HetBlock from ..blocks.helper_block import HelperBlock -from ..blocks.solved_block import SolvedBlock def block_sort(block_list, ignore_helpers=False, calibration=None, return_io=False): @@ -115,7 +112,7 @@ def construct_output_map(block_list, ignore_helpers=False, return_output_args=Fa continue # Find the relevant set of outputs corresponding to a block - if isinstance(block, SimpleBlock) or isinstance(block, HetBlock) or isinstance(block, HelperBlock) or isinstance(block, SolvedBlock): + if hasattr(block, "outputs"): outputs = block.outputs elif isinstance(block, dict): outputs = block.keys() From 5118fdd8d6d5648b0fdef5512e709fbfa6a8d315 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 10:47:15 -0600 Subject: [PATCH 056/288] Fix tests that used td_solve, since introduced breaking change swapping block_list and ss arguments --- sequence_jacobian/blocks/solved_block.py | 4 ++-- tests/base/test_transitional_dynamics.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 2e22daa..3c4312c 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -85,7 +85,7 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, # TODO: add H_U_factored caching of some kind # also, inefficient since we are repeatedly starting from the steady state, need option # to provide a guess (not a big deal with just SimpleBlocks, of course) - return nonlinear.td_solve(ss, self.block_list, list(self.unknowns.keys()), self.targets, + return nonlinear.td_solve(self.block_list, ss, list(self.unknowns.keys()), self.targets, exogenous=exogenous, monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) @@ -99,7 +99,7 @@ def impulse_linear(self, ss, exogenous, T=None): return self.jacobian(ss, list(exogenous.keys()), T=T).apply(exogenous) - def jacobian(self, ss, exogenous, T, output_list=None, save=False, use_saved=False): + def jacobian(self, ss, exogenous, T=300, output_list=None, save=False, use_saved=False): relevant_shocks = [i for i in self.inputs if i in exogenous] if not relevant_shocks: diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index cba5473..8d23df6 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -64,7 +64,7 @@ def test_hank_td(one_asset_hank_model): H_U = get_H_U(blocks, unknowns, targets, T, ss, use_saved=True) H_U_factored = utils.misc.factor(H_U) - td_nonlin = nonlinear.td_solve(ss, blocks, unknowns, targets, H_U_factored=H_U_factored, rstar=rstar, verbose=False) + td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, H_U_factored=H_U_factored, rstar=rstar, verbose=False) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -83,7 +83,7 @@ def test_two_asset_td(two_asset_hank_model): drstar = -0.0025 * 0.6 ** np.arange(T) rstar = ss["r"] + shock_size * drstar - td_nonlin = nonlinear.td_solve(ss, blocks, unknowns, targets, rstar=rstar, use_saved=True, verbose=False) + td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, rstar=rstar, use_saved=True, verbose=False) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_lin = shock_size * 100 * G['Y']['rstar'] @ drstar @@ -111,12 +111,12 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_model): drstar = -0.0025 * 0.6 ** np.arange(T) dY = 100 * G['Y']['rstar'] @ drstar - td_nonlin = nonlinear.td_solve(ss, blocks, unknowns, targets, + td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, rstar=ss['r']+drstar, use_saved=True, verbose=False) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_simple = 100 * G_simple['Y']['rstar'] @ drstar - td_nonlin_simple = nonlinear.td_solve(ss, blocks_simple, unknowns_simple, targets_simple, + td_nonlin_simple = nonlinear.td_solve(blocks_simple, ss, unknowns_simple, targets_simple, rstar=ss['r']+drstar, use_saved=True, verbose=False) dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) From 4bb192dafb71fc2c5a7f1d442c02516f3378a2c9 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 11:05:04 -0600 Subject: [PATCH 057/288] Fix issue with IgnoreVectors not converting to primitives when numeric_primitive is called --- sequence_jacobian/utilities/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index d9fa868..2833ac6 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -58,7 +58,7 @@ def numeric_primitive(instance): return instance elif isinstance(instance, np.ndarray): if np.issubdtype(instance.dtype, np.number): - return instance + return np.array(instance) else: raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance.dtype}," f" which is not a valid numeric type.") From 46e1d9f0ea533858cce8c02a56dba5e3053e923c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 11:36:06 -0600 Subject: [PATCH 058/288] (Breaking) Upgrade td_solve and remove deprecations to enforce consistency with how `exogenous` is passed in. Note below: It ended up being too messy allowing for the old convention of arbitrary kwarg passing of exogenous paths --- sequence_jacobian/blocks/combined_block.py | 4 +-- sequence_jacobian/blocks/het_block.py | 12 +++----- sequence_jacobian/blocks/simple_block.py | 14 ++++----- sequence_jacobian/blocks/solved_block.py | 16 ++++------ sequence_jacobian/devtools/deprecate.py | 17 ---------- sequence_jacobian/nonlinear.py | 36 +++++++++++----------- tests/base/test_jacobian.py | 4 +-- tests/base/test_transitional_dynamics.py | 29 +++++++++-------- 8 files changed, 54 insertions(+), 78 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 8c0f03d..e6d9550 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -152,8 +152,8 @@ def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviation """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - irf_nonlin_gen_eq = td_solve(self.blocks, ss, unknowns, targets, - exogenous={k: ss[k] + v for k, v in exogenous.items()}, **kwargs) + irf_nonlin_gen_eq = td_solve(self.blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, + unknowns=unknowns, targets=targets, **kwargs) # Default to percentage deviations from steady state. If the steady state value is zero, then just return # the level deviations from zero. diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 10b6272..e7093db 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -5,8 +5,6 @@ from .. import utilities as utils from ..jacobian.classes import JacobianDict -from ..devtools.deprecate import deprecated_shock_input_convention - def het(exogenous, policy, backward, backward_init=None): def decorator(back_step_fun): @@ -252,7 +250,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return ss def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=False, - grid_paths=None, **kwargs): + grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in kwargs are constant at their ss values. Analog to SimpleBlock.td. @@ -263,6 +261,9 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividua ---------- ss : dict all steady-state info, intended to be from .ss() + exogenous : dict of {str : array(T, ...)} + all time-varying inputs here, with first dimension being time + this must have same length T for all entries (all outputs will be calculated up to T) monotonic : [optional] bool flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us to use faster interpolation routines, otherwise use slower robust to nonmonotonicity @@ -270,9 +271,6 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividua return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies - exogenous : dict of {str : array(T, ...)} - all time-varying inputs here, with first dimension being time - this must have same length T for all entries (all outputs will be calculated up to T) Returns ---------- @@ -282,8 +280,6 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividua if returnindividual = True, additionally time paths for distribution and for all outputs of self.back_Step_fun on the full grid """ - exogenous = deprecated_shock_input_convention(exogenous, kwargs) - # infer T from exogenous, check that all shocks have same length shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 9485f19..08609ca 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -6,8 +6,6 @@ from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc -from ..devtools.deprecate import deprecated_shock_input_convention - '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -67,9 +65,9 @@ def _output_in_ss_format(self, **kwargs): else: return dict(zip(self.output_list, [misc.numeric_primitive(self.f(**kwargs))])) - def steady_state(self, calibration, **kwargs): + def steady_state(self, calibration): input_args = {k: ignore(v) for k, v in calibration.items()} - return self._output_in_ss_format(**input_args, **kwargs) + return self._output_in_ss_format(**input_args) def _output_in_td_format(self, **kwargs_new): """Returns output of the method td as a dict mapping output names to numeric primitives (scalars/vectors) @@ -91,9 +89,7 @@ def _output_in_td_format(self, **kwargs_new): else: return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def impulse_nonlinear(self, ss, exogenous=None, **kwargs): - exogenous = deprecated_shock_input_convention(exogenous, kwargs) - + def impulse_nonlinear(self, ss, exogenous=None): input_args = {} for k, v in exogenous.items(): if np.isscalar(v): @@ -171,7 +167,9 @@ def jacobian(self, ss, exogenous=None, T=None): return JacobianDict(J) def solve_steady_state(self, calibration, unknowns, targets, solver="", **kwargs): - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) + input_args = {k: ignore(v) for k, v in calibration.items() if k in self.inputs} + ss = super().solve_steady_state(input_args, unknowns, targets, solver=solver, **kwargs) + return {k: misc.numeric_primitive(v) for k, v in ss.items()} def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 3c4312c..cd8344f 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -6,8 +6,6 @@ from ..jacobian.classes import JacobianDict from ..blocks.simple_block import simple -from ..devtools.deprecate import deprecated_shock_input_convention - def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: @@ -79,15 +77,13 @@ def steady_state(self, calibration, consistency_check=True, ttol=1e-9, ctol=1e-9 solver=self.solver, **self.solver_kwargs) def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, - returnindividual=False, verbose=False, **kwargs): - exogenous = deprecated_shock_input_convention(exogenous, kwargs) - + returnindividual=False, verbose=False): # TODO: add H_U_factored caching of some kind - # also, inefficient since we are repeatedly starting from the steady state, need option - # to provide a guess (not a big deal with just SimpleBlocks, of course) - return nonlinear.td_solve(self.block_list, ss, list(self.unknowns.keys()), self.targets, - exogenous=exogenous, monotonic=monotonic, - returnindividual=returnindividual, verbose=verbose) + # also, inefficient since we are repeatedly starting from the steady state, need option + # to provide a guess (not a big deal with just SimpleBlocks, of course) + return nonlinear.td_solve(self.block_list, ss, exogenous=exogenous, + unknowns=list(self.unknowns.keys()), targets=self.targets, + monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) def impulse_linear(self, ss, exogenous, T=None): if T is None: diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py index c506fa3..caf4e88 100644 --- a/sequence_jacobian/devtools/deprecate.py +++ b/sequence_jacobian/devtools/deprecate.py @@ -8,20 +8,3 @@ # TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions # themselves. - - -# For impulse_nonlinear, td_solve, and td_map -def deprecated_shock_input_convention(exogenous, kwargs): - if kwargs: - warnings.warn("Passing shock paths/exogenous through kwargs is deprecated. Please explicitly specify" - " the dict of shocks in the keyword argument `exogenous`.", DeprecationWarning) - if exogenous is None: - return kwargs - else: - # If for whatever reason kwargs and exogenous is non-empty? - exogenous.update(kwargs) - else: - if exogenous is None: - return {} - else: - return exogenous diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index c738ab0..5a0440e 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -6,12 +6,10 @@ from .jacobian.drivers import get_H_U from .jacobian.support import pack_vectors, unpack_vectors -from .devtools.deprecate import deprecated_shock_input_convention - -def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_factored=None, monotonic=False, +def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factored=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, save=False, use_saved=False, - grid_paths=None, **kwargs): + grid_paths=None): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. @@ -20,9 +18,9 @@ def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_fa ---------- block_list : list, blocks in model (SimpleBlocks or HetBlocks) ss : dict, all steady-state information + exogenous : dict, all shocked Z go here, must all have same length T unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) - exogenous : dict, all shocked Z go here, must all have same length T H_U : [optional] array (nU*nU), Jacobian of targets with respect to unknowns H_U_factored : [optional] tuple, LU decomposition of H_U, save time by supplying this from utils.misc.factor() monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k @@ -38,7 +36,6 @@ def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_fa ---------- results : dict, return paths for all aggregate variables, plus individual outcomes of HetBlock if returnindividual """ - exogenous = deprecated_shock_input_convention(exogenous, kwargs) # check to make sure that exogenous are valid shocks for x in unknowns + targets: @@ -51,8 +48,8 @@ def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_fa break # initialize guess for unknowns to steady state length T - Us = {k: np.full(T, ss[k]) for k in unknowns} - Uvec = pack_vectors(Us, unknowns, T) + unknown_paths = {k: np.full(T, ss[k]) for k in unknowns} + Uvec = pack_vectors(unknown_paths, unknowns, T) # obtain H_U_factored if we don't have it already if H_U_factored is None: @@ -66,8 +63,9 @@ def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_fa # iterate until convergence for it in range(maxit): - results = td_map(block_list, ss, sort=sort, monotonic=monotonic, returnindividual=returnindividual, - grid_paths=grid_paths, **exogenous, **Us) + results = td_map(block_list, ss, exogenous, unknown_paths, sort=sort, + monotonic=monotonic, returnindividual=returnindividual, + grid_paths=grid_paths) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: print(f'On iteration {it}') @@ -79,20 +77,22 @@ def td_solve(block_list, ss, unknowns, targets, exogenous=None, H_U=None, H_U_fa # update guess U by -H_U^(-1) times errors H Hvec = pack_vectors(results, targets, T) Uvec -= misc.factored_solve(H_U_factored, Hvec) - Us = unpack_vectors(Uvec, unknowns, T) + unknown_paths = unpack_vectors(Uvec, unknowns, T) else: raise ValueError(f'No convergence after {maxit} backward iterations!') return results -def td_map(block_list, ss, exogenous=None, sort=None, monotonic=False, returnindividual=False, - grid_paths=None, **kwargs): +def td_map(block_list, ss, exogenous, unknowns=None, sort=None, + monotonic=False, returnindividual=False, grid_paths=None): """Helper for td_solve, calculates H(U, Z), where U and Z are in kwargs. Goes through block_list, topologically sorts the implied DAG, calculates H(U, Z), with missing paths always being interpreted as remaining at the steady state for a particular variable""" - exogenous = deprecated_shock_input_convention(exogenous, kwargs) + + if unknowns is None: + unknowns = {} hetoptions = {'monotonic': monotonic, 'returnindividual': returnindividual, 'grid_paths': grid_paths} @@ -101,7 +101,7 @@ def td_map(block_list, ss, exogenous=None, sort=None, monotonic=False, returnind sort = graph.block_sort(block_list, ignore_helpers=True) # initialize results - results = exogenous + results = {**exogenous, **unknowns} for n in sort: block = block_list[n] @@ -111,8 +111,8 @@ def td_map(block_list, ss, exogenous=None, sort=None, monotonic=False, returnind # if any input to the block has changed, run the block if not block.inputs.isdisjoint(results): - results.update(block.impulse_nonlinear(ss, **{k: v for k, v in hetoptions.items() - if k in misc.input_kwarg_list(block.impulse_nonlinear)}, - **{k: results[k] for k in block.inputs if k in results})) + results.update(block.impulse_nonlinear(ss, exogenous={k: results[k] for k in block.inputs if k in results}, + **{k: v for k, v in hetoptions.items() + if k in misc.input_kwarg_list(block.impulse_nonlinear)})) return results diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 54b1378..e7a9a58 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -139,12 +139,12 @@ def test_fake_news_v_direct_method(one_asset_hank_model): # (better than subtracting by ss since ss not exact) # monotonic=True lets us know there is monotonicity of policy rule, makes TD run faster # .td requires at least one input 'shock', so we put in steady-state w - td_noshock = household.td(ss, w=np.full(T, ss['w']), monotonic=True) + td_noshock = household.td(ss, exogenous={"w": np.full(T, ss['w'])}, monotonic=True) for i in shock_list: # simulate with respect to a shock at each date up to T for t in range(T): - td_out = household.td(ss, **{i: ss[i] + h * (np.arange(T) == t)}) + td_out = household.td(ss, exogenous={i: ss[i] + h * (np.arange(T) == t)}) # store results as column t of J[o][i] for each outcome o for o in output_list: diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 8d23df6..3ffc982 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -21,10 +21,10 @@ def test_rbc_td(rbc_model): dZ[:, 1] = np.concatenate((np.zeros(news), dZ[:-news, 0])) dC = 100 * G['C']['Z'] @ dZ / ss['C'] - td_nonlin = nonlinear.td_solve(ss=ss, block_list=blocks, unknowns=unknowns, targets=targets, - Z=ss["Z"]+dZ[:, 0], verbose=False) - td_nonlin_news = nonlinear.td_solve(ss=ss, block_list=blocks, unknowns=unknowns, targets=targets, - Z=ss["Z"]+dZ[:, 1], verbose=False) + td_nonlin = nonlinear.td_solve(block_list=blocks, ss=ss, exogenous={"Z": ss["Z"] + dZ[:, 0]}, + unknowns=unknowns, targets=targets, verbose=False) + td_nonlin_news = nonlinear.td_solve(block_list=blocks, ss=ss, exogenous={"Z": ss["Z"] + dZ[:, 1]}, + unknowns=unknowns, targets=targets, verbose=False) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_nonlin_news = 100 * (td_nonlin_news['C'] / ss['C'] - 1) @@ -40,10 +40,10 @@ def test_ks_td(krusell_smith_model): targets=targets, T=T, ss=ss) for shock_size, tol in [(0.01, 7e-3), (0.1, 0.6)]: - Z = ss['Z'] + shock_size * 0.8 ** np.arange(T) + Z = ss["Z"] + shock_size * 0.8 ** np.arange(T) - td_nonlin = nonlinear.td_solve(ss=ss, block_list=blocks, unknowns=unknowns, - targets=targets, monotonic=True, Z=Z, verbose=False) + td_nonlin = nonlinear.td_solve(ss=ss, block_list=blocks, exogenous={"Z": Z}, unknowns=unknowns, + targets=targets, monotonic=True, verbose=False) dr_nonlin = 10000 * (td_nonlin['r'] - ss['r']) dr_lin = 10000 * G['r']['Z'] @ (Z - ss['Z']) @@ -64,7 +64,8 @@ def test_hank_td(one_asset_hank_model): H_U = get_H_U(blocks, unknowns, targets, T, ss, use_saved=True) H_U_factored = utils.misc.factor(H_U) - td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, H_U_factored=H_U_factored, rstar=rstar, verbose=False) + td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": rstar}, + unknowns=unknowns, targets=targets, H_U_factored=H_U_factored, verbose=False) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -83,7 +84,8 @@ def test_two_asset_td(two_asset_hank_model): drstar = -0.0025 * 0.6 ** np.arange(T) rstar = ss["r"] + shock_size * drstar - td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, rstar=rstar, use_saved=True, verbose=False) + td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": rstar}, + unknowns=unknowns, targets=targets, use_saved=True, verbose=False) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_lin = shock_size * 100 * G['Y']['rstar'] @ drstar @@ -111,13 +113,14 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_model): drstar = -0.0025 * 0.6 ** np.arange(T) dY = 100 * G['Y']['rstar'] @ drstar - td_nonlin = nonlinear.td_solve(blocks, ss, unknowns, targets, - rstar=ss['r']+drstar, use_saved=True, verbose=False) + td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": ss["r"] + drstar}, + unknowns=unknowns, targets=targets, use_saved=True, verbose=False) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_simple = 100 * G_simple['Y']['rstar'] @ drstar - td_nonlin_simple = nonlinear.td_solve(blocks_simple, ss, unknowns_simple, targets_simple, - rstar=ss['r']+drstar, use_saved=True, verbose=False) + td_nonlin_simple = nonlinear.td_solve(blocks_simple, ss, exogenous={"rstar": ss["r"] + drstar}, + unknowns=unknowns_simple, targets=targets_simple, + use_saved=True, verbose=False) dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) From e8265aef57eee35bb1779ace60f0dba231cacb70 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 15:32:26 -0600 Subject: [PATCH 059/288] Complete implementation of parent Block type and confirm inheritance properly works for CombinedBlock and SimpleBlock usage of its methods --- sequence_jacobian/blocks/combined_block.py | 45 ----------------- sequence_jacobian/blocks/simple_block.py | 18 +------ sequence_jacobian/primitives.py | 57 ++++++++++++++++------ 3 files changed, 42 insertions(+), 78 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index e6d9550..2061dbc 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -7,8 +7,6 @@ from .. import utilities as utils from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default -from ..nonlinear import td_solve -from ..jacobian.drivers import get_G from ..jacobian.classes import JacobianDict from ..blocks.het_block import HetBlock @@ -147,46 +145,3 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar solver = provide_solver_default(unknowns) return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) - - def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, in_deviations=True, **kwargs): - """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - irf_nonlin_gen_eq = td_solve(self.blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, - unknowns=unknowns, targets=targets, **kwargs) - - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_gen_eq.items()} - else: - return irf_nonlin_gen_eq - - def solve_impulse_linear(self, ss, exogenous, unknowns, targets, T=None, in_deviations=True, **kwargs): - """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - if T is None: - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - J_gen_eq = get_G(self.blocks, list(exogenous.keys()), unknowns, targets, T=T, ss=ss, **kwargs) - irf_lin_gen_eq = J_gen_eq.apply(exogenous) - - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_gen_eq.items()} - else: - return irf_lin_gen_eq - - def solve_jacobian(self, ss, exogenous, unknowns, targets, T=None, **kwargs): - """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks - at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - J_gen_eq = get_G(self.blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) - return J_gen_eq - diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 08609ca..fe8ab7f 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -89,7 +89,7 @@ def _output_in_td_format(self, **kwargs_new): else: return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def impulse_nonlinear(self, ss, exogenous=None): + def impulse_nonlinear(self, ss, exogenous): input_args = {} for k, v in exogenous.items(): if np.isscalar(v): @@ -166,22 +166,6 @@ def jacobian(self, ss, exogenous=None, T=None): return JacobianDict(J) - def solve_steady_state(self, calibration, unknowns, targets, solver="", **kwargs): - input_args = {k: ignore(v) for k, v in calibration.items() if k in self.inputs} - ss = super().solve_steady_state(input_args, unknowns, targets, solver=solver, **kwargs) - return {k: misc.numeric_primitive(v) for k, v in ss.items()} - - def solve_impulse_nonlinear(self, ss, exogenous, unknowns, targets, - in_deviations=True, **kwargs): - pass - - def solve_impulse_linear(self, ss, exogenous, unknowns, targets, - T=None, in_deviations=True, **kwargs): - pass - - def solve_jacobian(self, ss, exogenous, unknowns, targets, T=None, **kwargs): - pass - def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 388c182..216b958 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -4,10 +4,11 @@ from abc import ABCMeta as NativeABCMeta from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List +import numpy as np from .steady_state.drivers import steady_state from .nonlinear import td_solve -from .jacobian.drivers import get_G +from .jacobian.drivers import get_impulse, get_G from .jacobian.classes import JacobianDict # Basic types @@ -72,47 +73,71 @@ def steady_state(self, *ss_args, **ss_kwargs) -> Dict[str, Union[Real, Array]]: @abc.abstractmethod def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - shocked_paths: Dict[str, Array], **kwargs) -> Dict[str, Array]: + exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: pass @abc.abstractmethod def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - shocked_paths: Dict[str, Array], **kwargs) -> Dict[str, Array]: + exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: pass @abc.abstractmethod def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous=None, T=None, **kwargs) -> JacobianDict: pass - # TODO: Implement this as a concrete method - # @abc.abstractmethod def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], solver: Optional[str] = "", **kwargs) -> Dict[str, Union[Real, Array]]: - if hasattr(self, "blocks_w_helpers"): - return steady_state(self.blocks_w_helpers, calibration, unknowns, targets, solver=solver, **kwargs) - else: - return steady_state([self], calibration, unknowns, targets, solver=solver, **kwargs) + """Evaluate a general equilibrium steady state of Block given a `calibration` + and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and + the target conditions that must hold in general equilibrium""" + blocks = self.blocks_w_helpers if hasattr(self, "blocks_w_helpers") else [self] + return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) - @abc.abstractmethod def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], in_deviations: Optional[bool] = True, **kwargs) -> Dict[str, Array]: - pass + """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + irf_nonlin_gen_eq = td_solve(blocks, ss, + exogenous={k: ss[k] + v for k, v in exogenous.items()}, + unknowns=unknowns, targets=targets, **kwargs) + + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_gen_eq.items()} + else: + return irf_nonlin_gen_eq - @abc.abstractmethod def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], T: Optional[int] = None, in_deviations: Optional[bool] = True, **kwargs) -> Dict[str, Array]: - pass + """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) + + # Default to percentage deviations from steady state. If the steady state value is zero, then just return + # the level deviations from zero. + if in_deviations: + return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_gen_eq.items()} + else: + return irf_lin_gen_eq - @abc.abstractmethod def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str], unknowns: List[str], targets: List[str], - T: Optional[int] = None, **kwargs) -> Dict[str, Array]: - pass + T: Optional[int] = None, **kwargs) -> JacobianDict: + """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks + at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) From 4bcb42a51a59f658d5bcc3da8eb2aef0fabab487 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 9 Mar 2021 16:32:34 -0600 Subject: [PATCH 060/288] Make HetBlock a child class of Block --- sequence_jacobian/blocks/het_block.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index e7093db..f7edd34 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -2,6 +2,7 @@ import copy import numpy as np +from ..primitives import Block from .. import utilities as utils from ..jacobian.classes import JacobianDict @@ -12,7 +13,7 @@ def decorator(back_step_fun): return decorator -class HetBlock: +class HetBlock(Block): """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since @@ -249,8 +250,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return ss - def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=False, - grid_paths=None): + def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in kwargs are constant at their ss values. Analog to SimpleBlock.td. @@ -368,7 +368,7 @@ def impulse_linear(self, ss, exogenous, T=None, **kwargs): return self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous) - def jacobian(self, ss, exogenous, T=300, output_list=None, h=1E-4, save=False, use_saved=False): + def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -397,6 +397,8 @@ def jacobian(self, ss, exogenous, T=300, output_list=None, h=1E-4, save=False, u """ # The default set of outputs are all outputs of the backward iteration function # except for the backward iteration variables themselves + if exogenous is None: + exogenous = list(self.inputs) if output_list is None: output_list = self.non_back_iter_outputs From de9bf6ef8eb24c36157c8b91b115bd59ecb1f9ef Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 12 Mar 2021 13:08:11 -0600 Subject: [PATCH 061/288] (WIP) Implement SolvedBlock as a child class of Block --- sequence_jacobian/blocks/solved_block.py | 93 +++++++++++++----------- sequence_jacobian/primitives.py | 2 + 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index cd8344f..27c2f04 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -1,10 +1,8 @@ import warnings -from .. import nonlinear -from ..steady_state.drivers import steady_state -from ..jacobian.drivers import get_G -from ..jacobian.classes import JacobianDict +from ..primitives import Block from ..blocks.simple_block import simple +from ..utilities import graph def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): @@ -25,7 +23,7 @@ def singleton_solved_block(f): return singleton_solved_block -class SolvedBlock: +class SolvedBlock(Block): """SolvedBlocks are mini SHADE models embedded as blocks inside larger SHADE models. When creating them, we need to provide the basic ingredients of a SHADE model: the list of @@ -38,8 +36,23 @@ class SolvedBlock: nonlinear transition path such that all internal targets of the mini SHADE model are zero. """ - def __init__(self, block_list, name, unknowns, targets, solver=None, solver_kwargs={}): - self.block_list = block_list + def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): + # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. + self._blocks_unsorted = blocks + + # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks + # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort + # Hence, we will cache that info upon first invocation of the steady_state + self._sorted_indices_w_o_helpers = graph.block_sort(blocks, ignore_helpers=True) + self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated + self._required = graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + + # User-facing attributes for accessing blocks + # .blocks_w_helpers meant to only interface with steady_state functionality + # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) + self.blocks_w_helpers = None + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] + self.name = name self.unknowns = unknowns self.targets = targets @@ -47,8 +60,8 @@ def __init__(self, block_list, name, unknowns, targets, solver=None, solver_kwar self.solver_kwargs = solver_kwargs # need to have inputs and outputs!!! - self.outputs = (set.union(*(b.outputs for b in block_list)) | set(list(self.unknowns.keys()))) - set(self.targets) - self.inputs = set.union(*(b.inputs for b in block_list)) - self.outputs + self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) + self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs # TODO: Deprecated methods, to be removed! def ss(self, **kwargs): @@ -69,38 +82,36 @@ def jac(self, ss, T=None, shock_list=None, **kwargs): return self.jacobian(ss, shock_list, T, **kwargs) def steady_state(self, calibration, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False): - if self.solver is None: - raise RuntimeError("Cannot call the ss method on this SolvedBlock without specifying a solver.") - else: - return steady_state(self.block_list, calibration, self.unknowns, self.targets, - consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, - solver=self.solver, **self.solver_kwargs) - - def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, + # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices + # accounting for HelperBlocks + if self._sorted_indices_w_helpers is None: + self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + calibration=calibration) + self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] + + return super().solve_steady_state(calibration, self.unknowns, self.targets, solver=self.solver, + consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) + + def impulse_nonlinear(self, ss, exogenous=None, in_deviations=True, monotonic=False, returnindividual=False, verbose=False): - # TODO: add H_U_factored caching of some kind - # also, inefficient since we are repeatedly starting from the steady state, need option - # to provide a guess (not a big deal with just SimpleBlocks, of course) - return nonlinear.td_solve(self.block_list, ss, exogenous=exogenous, - unknowns=list(self.unknowns.keys()), targets=self.targets, - monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - - def impulse_linear(self, ss, exogenous, T=None): - if T is None: - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - return self.jacobian(ss, list(exogenous.keys()), T=T).apply(exogenous) - - def jacobian(self, ss, exogenous, T=300, output_list=None, save=False, use_saved=False): + return super().solve_impulse_nonlinear(ss, exogenous=exogenous, + unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + in_deviations=in_deviations, monotonic=monotonic, + returnindividual=returnindividual, verbose=verbose) + + def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): + return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + T=T, in_deviations=in_deviations) + + def jacobian(self, ss, exogenous=None, T=300, outputs=None, save=False, use_saved=False): + if exogenous is None: + exogenous = list(self.inputs) + if outputs is None: + outputs = list(self.outputs) relevant_shocks = [i for i in self.inputs if i in exogenous] - if not relevant_shocks: - return JacobianDict({}) - else: - # H_U_factored caching could be helpful here too - return get_G(self.block_list, relevant_shocks, list(self.unknowns.keys()), self.targets, - T, ss, output_list, save=save, use_saved=use_saved) + return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + T=T, outputs=outputs, save=save, use_saved=use_saved) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 216b958..eb0b0a7 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -7,6 +7,7 @@ import numpy as np from .steady_state.drivers import steady_state +from .steady_state.support import provide_solver_default from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G from .jacobian.classes import JacobianDict @@ -93,6 +94,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks_w_helpers if hasattr(self, "blocks_w_helpers") else [self] + solver = solver if solver else provide_solver_default(unknowns) return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], From 8e7ff9784bc4da93457f6697e9b7fac1a97ec89a Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 9 Mar 2021 12:05:19 -0600 Subject: [PATCH 062/288] Added wiki (ignored by ssj git, it has its own) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index afc7adc..b102664 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ *.zip scripts/ +wiki/ From c0c789c1f1958ec6e6eee1c043a3444ab3c8d69d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 9 Mar 2021 16:35:19 -0600 Subject: [PATCH 063/288] Small change: aggregated hetoutputs can be targeted in calibration. --- sequence_jacobian/steady_state/drivers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 2cfdc16..ed811dd 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -79,6 +79,10 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False for i in topsorted: if not include_helpers and isinstance(blocks[i], HelperBlock): continue + # Want to see hetoutputs + elif hasattr(blocks[i], 'hetoutput') and blocks[i].hetoutput is not None: + outputs = eval_block_ss(blocks[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) + ss_values.update(misc.dict_diff(outputs, helper_outputs)) else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) From 99dec0836adb3590e7bc29cf83fd0112e3abb526 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 12:42:35 -0600 Subject: [PATCH 064/288] Nice __repr__ for SolvedBlocks. --- sequence_jacobian/blocks/solved_block.py | 3 +++ sequence_jacobian/jacobian/classes.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 27c2f04..0ecb8a0 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -63,6 +63,9 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs + def __repr__(self): + return f"" + # TODO: Deprecated methods, to be removed! def ss(self, **kwargs): warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 0264b93..726ee1e 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -423,4 +423,3 @@ def unpack(bigjac, outputs, inputs, T): for iI, I in enumerate(inputs): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] return JacobianDict(jacdict, outputs, inputs) - From ff57600fc10d89ddc45ec67392a0e65e73a14694 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 14:40:07 -0600 Subject: [PATCH 065/288] New ImpulseDict class created. --- sequence_jacobian/blocks/support/__init__.py | 3 +- sequence_jacobian/blocks/support/impulse.py | 60 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 sequence_jacobian/blocks/support/impulse.py diff --git a/sequence_jacobian/blocks/support/__init__.py b/sequence_jacobian/blocks/support/__init__.py index 51eaac3..1e46635 100644 --- a/sequence_jacobian/blocks/support/__init__.py +++ b/sequence_jacobian/blocks/support/__init__.py @@ -1 +1,2 @@ -"""Other classes and helpers to aid in the implementation of standard block functionality: .ss, .td, .jac""" +"""Other classes and helpers to aid standard block functionality: .steady_state, .impulse_linear, .impulse_nonlinear, +.jacobian""" diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py new file mode 100644 index 0000000..e5350e4 --- /dev/null +++ b/sequence_jacobian/blocks/support/impulse.py @@ -0,0 +1,60 @@ +"""ImpulseDict class for manipulating impulse responses.""" + +import numpy as np + + +class ImpulseDict: + def __init__(self, impulse, ss): + if isinstance(impulse, ImpulseDict): + self.impulse = impulse.impulse + self.ss = impulse.ss + else: + if not isinstance(impulse, dict) or not isinstance(ss, dict): + raise ValueError('ImpulseDicts are initialized with two dicts.') + self.impulse = impulse + self.ss = ss + + def __iter__(self): + return iter(self.impulse) + + def __mul__(self, x): + return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) + + def __rmul__(self, x): + return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) + + def __or__(self, other): + if not isinstance(other, ImpulseDict): + raise ValueError('Trying to merge an ImpulseDict with something else.') + if self.ss != other.ss: + raise ValueError('Trying to merge ImpulseDicts with different steady states.') + # make a copy, then add additional impulses + merged = type(self)(self.impulse, self.ss) + merged.impulse.update(other.impulse) + return merged + + def __getitem__(self, x): + # Behavior similar to pandas + if isinstance(x, str): + # case 1: impulses['C'] returns array + return self.impulse[x] + if isinstance(x, list): + # case 2: impulses[['C']] or impulses[['C', 'Y']] return smaller ImpulseDicts + return type(self)({k: self.impulse[k] for k in x}, self.ss) + + def normalize(self, x=None): + if x is None: + # default: normalize by steady state if not zero + impulse = {k: v/self.ss[k] if not np.isclose(self.ss[k], 0) else v for k, v in self.impulse.items()} + else: + # normalize by steady state of x + if x not in self.ss.keys(): + raise ValueError(f'Cannot normalize with {x}: steady state is unknown.') + elif np.isclose(self.ss[x], 0): + raise ValueError(f'Cannot normalize with {x}: steady state is zero.') + else: + impulse = {k: v / self.ss[x] for k, v in self.impulse.items()} + return type(self)(impulse, self.ss) + + def levels(self): + return type(self)({k: v + self.ss[k] for k, v in self.impulse.items()}, self.ss) From 0806d76e15b56e4f4d7c4858179097bd1eca73cf Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 15:17:56 -0600 Subject: [PATCH 066/288] solve_impulse_ methods now return ImpulseDicts --- sequence_jacobian/blocks/support/impulse.py | 10 ++++----- sequence_jacobian/primitives.py | 23 ++++++--------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index e5350e4..e40c03e 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -14,9 +14,6 @@ def __init__(self, impulse, ss): self.impulse = impulse self.ss = ss - def __iter__(self): - return iter(self.impulse) - def __mul__(self, x): return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) @@ -36,10 +33,10 @@ def __or__(self, other): def __getitem__(self, x): # Behavior similar to pandas if isinstance(x, str): - # case 1: impulses['C'] returns array + # case 1: ImpulseDict['C'] returns array return self.impulse[x] if isinstance(x, list): - # case 2: impulses[['C']] or impulses[['C', 'Y']] return smaller ImpulseDicts + # case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts return type(self)({k: self.impulse[k] for k in x}, self.ss) def normalize(self, x=None): @@ -58,3 +55,6 @@ def normalize(self, x=None): def levels(self): return type(self)({k: v + self.ss[k] for k, v in self.impulse.items()}, self.ss) + + def deviations(self): + return type(self)({k: v - self.ss[k] for k, v in self.impulse.items()}, self.ss) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index eb0b0a7..937368d 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -11,6 +11,7 @@ from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G from .jacobian.classes import JacobianDict +from .blocks.support.impulse import ImpulseDict # Basic types Array = Any @@ -100,7 +101,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], - in_deviations: Optional[bool] = True, **kwargs) -> Dict[str, Array]: + **kwargs) -> Dict[str, Array]: """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -108,31 +109,19 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], irf_nonlin_gen_eq = td_solve(blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, unknowns=unknowns, targets=targets, **kwargs) - - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_gen_eq.items()} - else: - return irf_nonlin_gen_eq + return ImpulseDict(irf_nonlin_gen_eq, ss).deviations() def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], - T: Optional[int] = None, in_deviations: Optional[bool] = True, - **kwargs) -> Dict[str, Array]: + T: Optional[int] = None, + **kwargs) -> ImpulseDict: """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) - - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_gen_eq.items()} - else: - return irf_lin_gen_eq + return ImpulseDict(irf_lin_gen_eq, ss) def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str], From 459584fc3ed371efe9c71e49243463bccd431ad1 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 16:03:46 -0600 Subject: [PATCH 067/288] get_impulse returns the shock as well. --- sequence_jacobian/jacobian/drivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index f77084f..5d5e31c 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -114,7 +114,7 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None J_curlyU_dU = forward_accumulate(curlyJs, dU, outputs, required) if outputs is None: outputs = J_curlyZ_dZ.keys() | J_curlyU_dU.keys() - return {o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs} + return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None, From 19bb08f0ea6e3fce9aadfce53a5cdf61e374c479 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 16:24:19 -0600 Subject: [PATCH 068/288] ImpulseDict works with `Block.impulse_linear` for all types of blocks. --- sequence_jacobian/blocks/combined_block.py | 12 ++++-------- sequence_jacobian/blocks/het_block.py | 3 ++- sequence_jacobian/blocks/simple_block.py | 3 ++- sequence_jacobian/blocks/support/impulse.py | 3 +++ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 2061dbc..76a41fe 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -3,6 +3,7 @@ from copy import deepcopy import numpy as np +from .support.impulse import ImpulseDict from ..primitives import Block from .. import utilities as utils from ..steady_state.drivers import eval_block_ss @@ -92,7 +93,7 @@ def impulse_nonlinear(self, ss, exogenous, in_deviations=True, **kwargs): else: return irf_nonlin_partial_eq - def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): + def impulse_linear(self, ss, exogenous, T=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from a steady_state, `ss`""" irf_lin_partial_eq = deepcopy(exogenous) @@ -100,14 +101,9 @@ def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T).items()}) + irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T)}) - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] if not np.isclose(ss[k], 0) else v for k, v in irf_lin_partial_eq.items()} - else: - return irf_lin_partial_eq + return ImpulseDict(irf_lin_partial_eq, ss) def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_saved=False): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 475accb..cdf8a36 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -2,6 +2,7 @@ import copy import numpy as np +from .support.impulse import ImpulseDict from ..primitives import Block from .. import utilities as utils from ..jacobian.classes import JacobianDict @@ -366,7 +367,7 @@ def impulse_linear(self, ss, exogenous, T=None, **kwargs): raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] - return self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous) + return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous), ss) def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index fe8ab7f..cfc1e98 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -2,6 +2,7 @@ import numpy as np from .support.simple_displacement import ignore, Displace, AccumulatedDerivative +from .support.impulse import ImpulseDict from ..primitives import Block from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc @@ -103,7 +104,7 @@ def impulse_nonlinear(self, ss, exogenous): return self._output_in_td_format(**input_args) def impulse_linear(self, ss, exogenous, T=None): - return self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous) + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous), ss) def jacobian(self, ss, exogenous=None, T=None): """Assemble nested dict of Jacobians diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index e40c03e..4c828f1 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -14,6 +14,9 @@ def __init__(self, impulse, ss): self.impulse = impulse self.ss = ss + def __iter__(self): + return iter(self.impulse.items()) + def __mul__(self, x): return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) From 7e78ff54d78bf086206e5dc604336ef1d3e9d7da Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 17:39:36 -0600 Subject: [PATCH 069/288] All `Block.impulse_nonlinear_` methods take deviations as inputs and return ImpulseDicts. --- sequence_jacobian/blocks/combined_block.py | 13 ++++--------- sequence_jacobian/blocks/het_block.py | 9 +++++---- sequence_jacobian/blocks/simple_block.py | 4 ++-- sequence_jacobian/nonlinear.py | 2 +- sequence_jacobian/primitives.py | 2 +- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 76a41fe..f0f2d47 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -76,22 +76,17 @@ def steady_state(self, calibration): ss_partial_eq.update(eval_block_ss(block, ss_partial_eq)) return ss_partial_eq - def impulse_nonlinear(self, ss, exogenous, in_deviations=True, **kwargs): + def impulse_nonlinear(self, ss, exogenous, **kwargs): """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state, `ss`""" - irf_nonlin_partial_eq = {k: ss[k] + v for k, v in exogenous.items()} + irf_nonlin_partial_eq = deepcopy(exogenous) for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs).items()}) + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) - # Default to percentage deviations from steady state. If the steady state value is zero, then just return - # the level deviations from zero. - if in_deviations: - return {k: v/ss[k] - 1 if not np.isclose(ss[k], 0) else v for k, v in irf_nonlin_partial_eq.items()} - else: - return irf_nonlin_partial_eq + return ImpulseDict(irf_lin_partial_eq, ss) def impulse_linear(self, ss, exogenous, T=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index cdf8a36..542b80b 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -263,7 +263,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal ss : dict all steady-state info, intended to be from .ss() exogenous : dict of {str : array(T, ...)} - all time-varying inputs here, with first dimension being time + all time-varying inputs here (in deviations), with first dimension being time this must have same length T for all entries (all outputs will be calculated up to T) monotonic : [optional] bool flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us @@ -311,7 +311,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal backdict = ss.copy() for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: v[t,...] for k, v in exogenous.items()}) + backdict.update({k: ss[k] + v[t,...] for k, v in exogenous.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -356,9 +356,10 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal # return either this, or also include distributional information if returnindividual: - return {**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, 'D': D_path} + return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, + 'D': D_path}, ss) else: - return {**aggregates, **aggregate_hetoutputs} + return ImpulseDict({**aggregates, **aggregate_hetoutputs}, ss) def impulse_linear(self, ss, exogenous, T=None, **kwargs): # infer T from exogenous, check that all shocks have same length diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index cfc1e98..4111f18 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -95,13 +95,13 @@ def impulse_nonlinear(self, ss, exogenous): for k, v in exogenous.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') - input_args[k] = Displace(v, ss=ss.get(k, None), name=k) + input_args[k] = Displace(v + ss[k], ss=ss.get(k, None), name=k) for k in self.input_list: if k not in input_args: input_args[k] = ignore(ss[k]) - return self._output_in_td_format(**input_args) + return ImpulseDict(self._output_in_td_format(**input_args), ss) def impulse_linear(self, ss, exogenous, T=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous), ss) diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 5a0440e..9197d1f 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -111,7 +111,7 @@ def td_map(block_list, ss, exogenous, unknowns=None, sort=None, # if any input to the block has changed, run the block if not block.inputs.isdisjoint(results): - results.update(block.impulse_nonlinear(ss, exogenous={k: results[k] for k in block.inputs if k in results}, + results.update(block.impulse_nonlinear(ss, exogenous={k: results[k] - ss[k] for k in block.inputs if k in results}, **{k: v for k, v in hetoptions.items() if k in misc.input_kwarg_list(block.impulse_nonlinear)})) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 937368d..465a205 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -109,7 +109,7 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], irf_nonlin_gen_eq = td_solve(blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, unknowns=unknowns, targets=targets, **kwargs) - return ImpulseDict(irf_nonlin_gen_eq, ss).deviations() + return ImpulseDict(irf_nonlin_gen_eq, ss) def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], From 9b99296584a4f9655835a6530cb84b72fd575074 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 11 Mar 2021 17:45:22 -0600 Subject: [PATCH 070/288] Forgot about SolvedBlocks. They also return ImpulseDicts now. From 9532659e5a0e00db4ac578b83f52fe6ff5a15d05 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 12 Mar 2021 16:51:47 -0600 Subject: [PATCH 071/288] Fix CombinedBlock impulse_nonlinear implementation. Notes below: Because block.impulse_nonlinear returns impulse responses in levels we need to get it back in deviations to feed in as `exogenous`, since we have now adopted the convention that we symmetrically provide `exogenous` to both `impulse_linear` and `impulse_nonlinear` in deviation units. Thus, we need to construct the ImpulseDict to be returned at the very end back in levels() --- sequence_jacobian/blocks/combined_block.py | 4 ++-- sequence_jacobian/blocks/solved_block.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index f0f2d47..4c1a8d8 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -84,9 +84,9 @@ def impulse_nonlinear(self, ss, exogenous, **kwargs): input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) + irf_nonlin_partial_eq.update({k: v - ss[k] for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) - return ImpulseDict(irf_lin_partial_eq, ss) + return ImpulseDict(irf_nonlin_partial_eq, ss).levels() def impulse_linear(self, ss, exogenous, T=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 0ecb8a0..3546a60 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -95,18 +95,17 @@ def steady_state(self, calibration, consistency_check=True, ttol=1e-9, ctol=1e-9 return super().solve_steady_state(calibration, self.unknowns, self.targets, solver=self.solver, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) - def impulse_nonlinear(self, ss, exogenous=None, in_deviations=True, monotonic=False, + def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, returnindividual=False, verbose=False): return super().solve_impulse_nonlinear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - in_deviations=in_deviations, monotonic=monotonic, - returnindividual=returnindividual, verbose=verbose) + monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - def impulse_linear(self, ss, exogenous, T=None, in_deviations=True): + def impulse_linear(self, ss, exogenous, T=None): return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, in_deviations=in_deviations) + T=T) def jacobian(self, ss, exogenous=None, T=300, outputs=None, save=False, use_saved=False): if exogenous is None: From 8181add96a9181f2ca28fda9171fc810f064f87c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 12 Mar 2021 16:52:02 -0600 Subject: [PATCH 072/288] Fix tests to be compatible with new ImpulseDict class --- tests/base/test_jacobian.py | 4 ++-- tests/base/test_simple_block.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index e7a9a58..4cfe921 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -139,12 +139,12 @@ def test_fake_news_v_direct_method(one_asset_hank_model): # (better than subtracting by ss since ss not exact) # monotonic=True lets us know there is monotonicity of policy rule, makes TD run faster # .td requires at least one input 'shock', so we put in steady-state w - td_noshock = household.td(ss, exogenous={"w": np.full(T, ss['w'])}, monotonic=True) + td_noshock = household.td(ss, exogenous={"w": np.zeros(T)}, monotonic=True) for i in shock_list: # simulate with respect to a shock at each date up to T for t in range(T): - td_out = household.td(ss, exogenous={i: ss[i] + h * (np.arange(T) == t)}) + td_out = household.td(ss, exogenous={i: h * (np.arange(T) == t)}) # store results as column t of J[o][i] for each outcome o for o in output_list: diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 32025a4..b120d9a 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -38,8 +38,8 @@ def test_block_consistency(block, ss): ss_results = block.ss(**ss) # now if we put in constant inputs, td should give us the same! - td_results = block.td(ss, **{k: np.full(20, v) for k, v in ss.items()}) - for k, v in td_results.items(): + td_results = block.td(ss, **{k: np.zeros(20) for k in ss.keys()}) + for k, v in td_results.impulse.items(): assert np.all(v == ss_results[k]) # now get the Jacobian @@ -51,11 +51,11 @@ def test_block_consistency(block, ss): h = 1E-5 all_shocks = {i: np.random.rand(10) for i in block.input_list} - td_up = block.td(ss, **{i: ss[i] + h*shock for i, shock in all_shocks.items()}) - td_dn = block.td(ss, **{i: ss[i] - h*shock for i, shock in all_shocks.items()}) + td_up = block.td(ss, **{i: h*shock for i, shock in all_shocks.items()}) + td_dn = block.td(ss, **{i: -h*shock for i, shock in all_shocks.items()}) - linear_impulses = {o: (td_up[o] - td_dn[o])/(2*h) for o in td_up} - linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up} + linear_impulses = {o: (td_up.impulse[o] - td_dn.impulse[o])/(2*h) for o in td_up.impulse} + linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up.impulse} for o in linear_impulses: assert np.all(np.abs(linear_impulses[o] - linear_impulses_from_jac[o]) < 1E-5) From 087eef16d45342dd0572a5530e00d3d6ea210379 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 15 Mar 2021 14:44:27 -0500 Subject: [PATCH 073/288] ImpulseDict: simple repr and tests. --- sequence_jacobian/blocks/support/impulse.py | 3 ++ sequence_jacobian/primitives.py | 2 +- tests/base/test_jacobian.py | 3 +- tests/base/test_public_classes.py | 34 +++++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 tests/base/test_public_classes.py diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index 4c828f1..18dd39e 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -14,6 +14,9 @@ def __init__(self, impulse, ss): self.impulse = impulse self.ss = ss + def __repr__(self): + return f'' + def __iter__(self): return iter(self.impulse.items()) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 465a205..d6f36ba 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -101,7 +101,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], - **kwargs) -> Dict[str, Array]: + **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 4cfe921..5358825 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -101,7 +101,6 @@ def test_fake_news_v_actual(one_asset_hank_model): F[1:, ...] = curlyPs[o].reshape(T-1, -1) @ curlyDs[i].reshape(T, -1).T Fs[o.capitalize()][i] = F - impulse = Fs['C']['w'][:10, 1].copy() # start with fake news impulse impulse[1:10] += Js['C']['w'][:9, 0] # add unanticipated impulse, shifted by 1 @@ -128,7 +127,7 @@ def test_fake_news_v_direct_method(one_asset_hank_model): household = blocks[0] T = 40 - shock_list = ('r') + shock_list = 'r' output_list = household.non_back_iter_outputs h = 1E-4 diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py new file mode 100644 index 0000000..84b221a --- /dev/null +++ b/tests/base/test_public_classes.py @@ -0,0 +1,34 @@ +"""Test public-facing classes""" + +import numpy as np +from sequence_jacobian import create_model +from sequence_jacobian.blocks.support.impulse import ImpulseDict + + +def test_impulsedict(krusell_smith_model): + blocks, exogenous, unknowns, targets, ss = krusell_smith_model + household, firm, mkt_clearing, _, _, _ = blocks + T = 200 + + # Linearized impulse responses as deviations, nonlinear as levels + ks = create_model(*blocks, name='KS') + ir_lin = ks.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) + ir_nonlin = ks.solve_impulse_nonlinear(ss, {'Z': 0.01 * 0.5 ** np.arange(T)}, unknowns, targets) + + # Get method + assert isinstance(ir_lin, ImpulseDict) + assert isinstance(ir_lin[['C']], ImpulseDict) + assert isinstance(ir_lin['C'], np.ndarray) + + # Merge method + temp = ir_lin[['C', 'K']] | ir_lin[['r']] + assert list(temp.impulse.keys()) == ['C', 'K', 'r'] + + # Normalize and scalar multiplication + dC1 = 100 * ir_lin['C'] / ss['C'] + dC2 = 100 * ir_lin[['C']].normalize() + assert np.allclose(dC1, dC2['C']) + + # Levels and deviations + assert np.linalg.norm(ir_nonlin.deviations()['C'] - ir_lin['C']) < 1E-4 + assert np.linalg.norm(ir_nonlin['C'] - ir_lin.levels()['C']) < 1E-4 From 383c1faae5d6f3be1253cc8afaff323a23ad3f50 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 17:20:57 -0500 Subject: [PATCH 074/288] Deprecate `output_list` kwarg in favor of `outputs` in HetBlock --- sequence_jacobian/blocks/het_block.py | 21 ++++++++++++--------- sequence_jacobian/devtools/deprecate.py | 12 ++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 542b80b..3a5bd9a 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -6,6 +6,7 @@ from ..primitives import Block from .. import utilities as utils from ..jacobian.classes import JacobianDict +from ..devtools.deprecate import rename_output_list_to_outputs def het(exogenous, policy, backward, backward_init=None): @@ -370,7 +371,7 @@ def impulse_linear(self, ss, exogenous, T=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=False, use_saved=False): + def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h=1E-4, save=False, use_saved=False): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -381,7 +382,7 @@ def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=Fal number of time periods for T*T Jacobian exogenous : list of str names of input variables to differentiate wrt (main cost scales with # of inputs) - output_list : list of str + outputs : list of str names of output variables to get derivatives of, if not provided assume all outputs of self.back_step_fun except self.back_iter_vars h : [optional] float @@ -401,15 +402,17 @@ def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=Fal # except for the backward iteration variables themselves if exogenous is None: exogenous = list(self.inputs) - if output_list is None: - output_list = self.non_back_iter_outputs + if outputs is None or output_list is None: + outputs = self.non_back_iter_outputs + else: + outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] # if we're supposed to use saved Jacobian, extract T-by-T submatrices for each (o,i) if use_saved: return utils.misc.extract_nested_dict(savedA=self.saved['J'], - keys1=[o.capitalize() for o in output_list], + keys1=[o.capitalize() for o in outputs], keys2=relevant_shocks, shape=(T, T)) # step 0: preliminary processing of steady state @@ -420,20 +423,20 @@ def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=Fal # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in relevant_shocks: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, output_list, ssin_dict, ssout_list, + curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, ss['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, h, ss_for_hetinput) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o curlyPs = {} - for o in output_list: + for o in outputs: curlyPs[o] = self.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair F, J = {}, {} - for o in output_list: + for o in outputs: for i in relevant_shocks: if o.capitalize() not in F: F[o.capitalize()] = {} @@ -443,7 +446,7 @@ def jacobian(self, ss, exogenous=None, T=300, output_list=None, h=1E-4, save=Fal J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) if save: - self.saved_shock_list, self.saved_output_list = relevant_shocks, output_list + self.saved_shock_list, self.saved_output_list = relevant_shocks, outputs self.saved = {'curlyYs': curlyYs, 'curlyDs': curlyDs, 'curlyPs': curlyPs, 'F': F, 'J': J} return JacobianDict(J) diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py index caf4e88..e7b4cb1 100644 --- a/sequence_jacobian/devtools/deprecate.py +++ b/sequence_jacobian/devtools/deprecate.py @@ -8,3 +8,15 @@ # TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions # themselves. + + +def rename_output_list_to_outputs(outputs=None, output_list=None): + if output_list is not None: + warnings.warn("The output_list kwarg has been deprecated and replaced with the outputs kwarg.", + DeprecationWarning) + if outputs is None: + return output_list + else: + return list(set(outputs) | set(output_list)) + else: + return outputs From f33968f9bf545a03788d07256e5805a426237476 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 17:21:21 -0500 Subject: [PATCH 075/288] Remove HetBlock dependency in CombinedBlock --- sequence_jacobian/blocks/combined_block.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 4c1a8d8..adb519e 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -9,7 +9,6 @@ from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict -from ..blocks.het_block import HetBlock def combine(*args, name="", model_alias=False): @@ -107,13 +106,11 @@ def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_sav exogenous = list(self.inputs) if outputs is None: outputs = self.outputs + kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "save": save, "use_saved": use_saved} J_partial_eq = JacobianDict.identity(exogenous) for block in self.blocks: - if isinstance(block, HetBlock): - curlyJ = block.jacobian(ss, exogenous, T, save=save, use_saved=use_saved).complete() - else: - curlyJ = block.jacobian(ss, exogenous, T).complete() + curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() # If we want specific list of outputs, restrict curlyJ to that before continuing curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] From ebd2a8afcd1bf4effcec88c03229795df6b5e3df Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 17:28:51 -0500 Subject: [PATCH 076/288] Make partial equilibrium .jacobian method for CombinedBlock return outputs requested, not necessarily including exogenous --- sequence_jacobian/blocks/combined_block.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index adb519e..750b6d3 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -108,13 +108,15 @@ def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_sav outputs = self.outputs kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "save": save, "use_saved": use_saved} - J_partial_eq = JacobianDict.identity(exogenous) - for block in self.blocks: + for i, block in enumerate(self.blocks): curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() # If we want specific list of outputs, restrict curlyJ to that before continuing curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] - J_partial_eq.update(curlyJ.compose(J_partial_eq)) + if i == 0: + J_partial_eq = curlyJ.compose(JacobianDict.identity(exogenous)) + else: + J_partial_eq.update(curlyJ.compose(J_partial_eq)) return J_partial_eq From 2cb8029deb6fb332a5aea7ff08baaa6e7af4c6d2 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 20:32:45 -0500 Subject: [PATCH 077/288] Make Block a more flexible parent class, allowing for steady_state, jacobian, and impulse_* to not be implemented --- sequence_jacobian/primitives.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index d6f36ba..2fd56f8 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -4,7 +4,6 @@ from abc import ABCMeta as NativeABCMeta from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List -import numpy as np from .steady_state.drivers import steady_state from .steady_state.support import provide_solver_default @@ -42,10 +41,11 @@ def __call__(cls, *args, **kwargs): } if abstract_attributes: raise NotImplementedError( - "Can't instantiate abstract class {} with" - " abstract attributes: {}".format( + "Cannot instantiate abstract class `{}` with" + " abstract attributes: `{}`.\n" + "Define concrete implementations of these attributes in the child class prior to preceding.".format( cls.__name__, - ', '.join(abstract_attributes) + '`, `'.join(abstract_attributes) ) ) return instance @@ -69,23 +69,21 @@ def outputs(self): # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical # input and output argument structure - @abc.abstractmethod - def steady_state(self, *ss_args, **ss_kwargs) -> Dict[str, Union[Real, Array]]: - pass + def steady_state(self, calibration: Dict[str, Union[Real, Array]], + **kwargs) -> Dict[str, Union[Real, Array]]: + raise NotImplementedError(f'{type(self)} does not implement .steady_state()') - @abc.abstractmethod def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: - pass + raise NotImplementedError(f'{type(self)} does not implement .impulse_nonlinear()') - @abc.abstractmethod def impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: - pass + raise NotImplementedError(f'{type(self)} does not implement .impulse_linear()') - @abc.abstractmethod - def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous=None, T=None, **kwargs) -> JacobianDict: - pass + def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = None, + T: int = None, **kwargs) -> JacobianDict: + raise NotImplementedError(f'{type(self)} does not implement .jacobian()') def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], From 82b54508c0eb7eabaa2a103416daaedd11ba0bc4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 20:38:02 -0500 Subject: [PATCH 078/288] Fix CombinedBlock to allow for kwargs to be passed into steady_state (which are potentially needed in eval_block_ss for HetBlocks) --- sequence_jacobian/blocks/combined_block.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 750b6d3..b6341c7 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -1,7 +1,6 @@ """CombinedBlock class and the combine function to generate it""" from copy import deepcopy -import numpy as np from .support.impulse import ImpulseDict from ..primitives import Block @@ -61,7 +60,7 @@ def __repr__(self): else: return f"" - def steady_state(self, calibration): + def steady_state(self, calibration, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks @@ -72,7 +71,7 @@ def steady_state(self, calibration): ss_partial_eq = deepcopy(calibration) for block in self.blocks_w_helpers: - ss_partial_eq.update(eval_block_ss(block, ss_partial_eq)) + ss_partial_eq.update(eval_block_ss(block, ss_partial_eq, **kwargs)) return ss_partial_eq def impulse_nonlinear(self, ss, exogenous, **kwargs): From dfa7ff9f48f110ad2b5718ecdfd127341693b28e Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 16 Mar 2021 20:38:28 -0500 Subject: [PATCH 079/288] Remove extraneous docstring --- sequence_jacobian/blocks/simple_block.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 4111f18..dc86503 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -118,8 +118,6 @@ def jacobian(self, ss, exogenous=None, T=None): if omitted, more efficient SimpleSparse objects returned exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs - h : float, optional - radius for symmetric numerical differentiation Returns ------- From 58c65e3c30cb2dfd9212168f701377d1d1b0b368 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 17 Mar 2021 10:24:47 -0500 Subject: [PATCH 080/288] Rename block_list to blocks in the jacobian driver functions and remove commented out code --- sequence_jacobian/jacobian/drivers.py | 52 ++++++++++----------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 5d5e31c..08e9cc1 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -16,17 +16,17 @@ ''' -def get_H_U(block_list, unknowns, targets, T, ss=None, save=False, use_saved=False): +def get_H_U(blocks, unknowns, targets, T, ss=None, save=False, use_saved=False): """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. Parameters ---------- - block_list : list, simple blocks, het blocks, or jacdicts + blocks : list, simple blocks, het blocks, or jacdicts unknowns : list of str, names of unknowns in DAG targets : list of str, names of targets in DAG T : int, truncation horizon (if asymptotic, truncation horizon for backward iteration in HetBlocks) - ss : [optional] dict, steady state required if block_list contains any non-jacdicts + ss : [optional] dict, steady state required if blocks contains any non-jacdicts save : [optional] bool, flag for saving Jacobians inside HetBlocks use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks @@ -40,7 +40,7 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, save=False, use_saved=Fal """ # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(block_list, unknowns, ss, T, save, use_saved) + curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, save, use_saved) # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) @@ -49,7 +49,7 @@ def get_H_U(block_list, unknowns, targets, T, ss=None, save=False, use_saved=Fal return H_U_unpacked[targets, unknowns].pack(T) -def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None, +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_U=None, H_U_factored=None, save=False, use_saved=False): """Get a single general equilibrium impulse response. @@ -58,12 +58,12 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None Parameters ---------- - block_list : list, simple blocks or jacdicts + blocks : list, simple blocks or jacdicts dZ : dict, path of an exogenous variable unknowns : list of str, names of unknowns in DAG targets : list of str, names of targets in DAG T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if block_list contains non-jacdicts + ss : [optional] dict, steady state required if blocks contains non-jacdicts outputs : [optional] list of str, variables we want impulse responses for H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) @@ -80,7 +80,7 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None T = len(x) break - curlyJs, required = curlyJ_sorted(block_list, unknowns + list(dZ.keys()), ss, T, + curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, save=save, use_saved=use_saved) # step 1: if not provided, do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) @@ -98,7 +98,6 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None # step 3: solve H_UdU = -H_ZdZ for dU if H_U is None and H_U_factored is None: H_U = H_U_unpacked[targets, unknowns].pack(T) - # H_U = pack_jacobians(H_U_unpacked, unknowns, targets, T) H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) @@ -117,7 +116,7 @@ def get_impulse(block_list, dZ, unknowns, targets, T=None, ss=None, outputs=None return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} -def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None, +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None, save=False, use_saved=False): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -129,12 +128,12 @@ def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None Parameters ---------- - block_list : list, simple blocks or jacdicts + blocks : list, simple blocks or jacdicts exogenous : list of str, names of exogenous shocks in DAG unknowns : list of str, names of unknowns in DAG targets : list of str, names of targets in DAG T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if block_list contains non-jacdicts + ss : [optional] dict, steady state required if blocks contains non-jacdicts outputs : [optional] list of str, variables we want impulse responses for H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) @@ -147,7 +146,7 @@ def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None """ # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(block_list, unknowns + exogenous, ss, T, + curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, save=save, use_saved=use_saved) # step 2: do (matrix) forward accumulation to get @@ -159,9 +158,7 @@ def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None # step 3: solve for G^U, unpack if H_U is None and H_U_factored is None: H_U = J_curlyH_U[targets, unknowns].pack(T) - # H_U = pack_jacobians(J_curlyH_U, unknowns, targets, T) H_Z = J_curlyH_Z[targets, exogenous].pack(T) - # H_Z = pack_jacobians(J_curlyH_Z, exogenous, targets, T) if H_U_factored is None: G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) @@ -176,17 +173,17 @@ def get_G(block_list, exogenous, unknowns, targets, T=300, ss=None, outputs=None return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=False): +def curlyJ_sorted(blocks, inputs, ss=None, T=None, save=False, use_saved=False): """ Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs and with respect to outputs of other blocks Parameters ---------- - block_list : list, simple blocks or jacdicts + blocks : list, simple blocks or jacdicts inputs : list, input names we need to differentiate with respect to - ss : [optional] dict, steady state, needed if block_list includes blocks themselves - T : [optional] int, horizon for differentiation, needed if block_list includes hetblock itself + ss : [optional] dict, steady state, needed if blocks includes blocks themselves + T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself save : [optional] bool, flag for saving Jacobians inside HetBlocks use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks @@ -197,8 +194,8 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=Fal """ # step 1: get topological sort and required - topsorted = graph.block_sort(block_list, ignore_helpers=True) - required = graph.find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=True) + topsorted = graph.block_sort(blocks, ignore_helpers=True) + required = graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) # Remove any vector-valued outputs that are intermediate inputs, since we don't want # to compute Jacobians with respect to vector-valued variables @@ -210,7 +207,7 @@ def curlyJ_sorted(block_list, inputs, ss=None, T=None, save=False, use_saved=Fal curlyJs = [] shocks = set(inputs) | required for num in topsorted: - block = block_list[num] + block = blocks[num] if hasattr(block, 'jacobian'): jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T, "save": save, "use_saved": use_saved}.items() @@ -280,26 +277,15 @@ def forward_accumulate(curlyJs, inputs, outputs=None, required=None): curlyJ = JacobianDict(curlyJ).complete() if alloutputs is not None: # if we want specific list of outputs, restrict curlyJ to that before continuing - # curlyJ = {k: v for k, v in curlyJ.items() if k in alloutputs} curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] if jacflag: - # out.update(compose_jacobians(out, curlyJ)) out.update(curlyJ.compose(out)) else: - # out.update(apply_jacobians(curlyJ, out)) out.update(curlyJ.apply(out)) if outputs is not None: # if we want specific list of outputs, restrict to that # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) - # return {k: out[k] for k in outputs if k in out} return out[[k for k in outputs if k in out.outputs]] else: return out - # if jacflag: - # # default behavior for Jacobian case: return all Jacobians we used/calculated along the way - # # except the (redundant) IdentityMatrix objects mapping inputs to themselves - # return {k: v for k, v in out.items() if k not in inputs} - # else: - # # default behavior for case where we're calculating paths: return everything, including inputs - # return out From 2c04ec77c793f4897a27288bced7c81a022431a4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 17 Mar 2021 14:59:45 -0500 Subject: [PATCH 081/288] Remove HelperBlock and replace it with helper_indices convention --- sequence_jacobian/__init__.py | 1 - sequence_jacobian/blocks/combined_block.py | 24 +++-- sequence_jacobian/blocks/helper_block.py | 44 --------- sequence_jacobian/models/hank.py | 3 +- sequence_jacobian/models/krusell_smith.py | 3 +- sequence_jacobian/models/rbc.py | 3 +- sequence_jacobian/steady_state/drivers.py | 29 ++++-- sequence_jacobian/utilities/graph.py | 108 +++++++++++---------- 8 files changed, 96 insertions(+), 119 deletions(-) delete mode 100644 sequence_jacobian/blocks/helper_block.py diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index eeaa0a7..57c538d 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -8,7 +8,6 @@ from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput -from .blocks.helper_block import helper from .blocks.solved_block import solved from .blocks.combined_block import combine from .blocks.support.simple_displacement import apply_function diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index b6341c7..8a41ac8 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -10,9 +10,9 @@ from ..jacobian.classes import JacobianDict -def combine(*args, name="", model_alias=False): +def combine(*args, name="", helper_indices=None, model_alias=False): # TODO: Implement a check that all args are child types of AbstractBlock, when that is properly implemented - return CombinedBlock(*args, name=name, model_alias=model_alias) + return CombinedBlock(*args, name=name, helper_indices=helper_indices, model_alias=model_alias) class CombinedBlock(Block): @@ -22,16 +22,18 @@ class CombinedBlock(Block): # To users: Do *not* manually change the attributes via assignment. Instantiating a # CombinedBlock has some automated features that are inferred from initial instantiation but not from # re-assignment of attributes post-instantiation. - def __init__(self, *blocks, name="", model_alias=False): + def __init__(self, *blocks, name="", helper_indices=None, model_alias=False): + # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. self._blocks_unsorted = blocks - # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks - # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort + # Upon instantiation, we only have enough information to conduct a sort ignoring helper blocks + # since we need a `calibration` to resolve cyclic dependencies when including helper blocks in a topological sort # Hence, we will cache that info upon first invocation of the steady_state - self._sorted_indices_w_o_helpers = utils.graph.block_sort(blocks, ignore_helpers=True) + self.helper_indices = helper_indices if helper_indices is not None else [] + self._sorted_indices_w_o_helpers = utils.graph.block_sort([b for i, b in enumerate(blocks) if i not in self.helper_indices]) self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated - self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + self._required = utils.graph.find_outputs_that_are_intermediate_inputs([b for i, b in enumerate(blocks) if i not in self.helper_indices]) # User-facing attributes for accessing blocks # .blocks_w_helpers meant to only interface with steady_state functionality @@ -63,9 +65,10 @@ def __repr__(self): def steady_state(self, calibration, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for HelperBlocks + # accounting for helper blocks if self._sorted_indices_w_helpers is None: self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + helper_indices=self.helper_indices, calibration=calibration) self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] @@ -124,13 +127,14 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for HelperBlocks + # accounting for helper blocks if self._sorted_indices_w_helpers is None: self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + helper_indices=self.helper_indices, calibration=calibration) self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] if solver is None: solver = provide_solver_default(unknowns) - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, sort_blocks=False, **kwargs) diff --git a/sequence_jacobian/blocks/helper_block.py b/sequence_jacobian/blocks/helper_block.py deleted file mode 100644 index bbb93ef..0000000 --- a/sequence_jacobian/blocks/helper_block.py +++ /dev/null @@ -1,44 +0,0 @@ -"""HelperBlock class and @helper decorator to generate it""" - -import warnings - -from ..utilities import misc - - -def helper(f): - return HelperBlock(f) - - -class HelperBlock: - """ A block for providing pre-computed solutions in lieu of solving for variables within the DAG. - Key methods are .ss, .td, and .jac, like HetBlock. - """ - - def __init__(self, f): - self.f = f - self.name = f.__name__ - self.input_list = misc.input_list(f) - self.output_list = misc.output_list(f) - self.inputs = set(self.input_list) - self.outputs = set(self.output_list) - - def __repr__(self): - return f"" - - # TODO: Deprecated methods, to be removed! - def ss(self, *args, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(*args, **kwargs) - - def _output_in_ss_format(self, calibration, **kwargs): - """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single - numeric primitive, as opposed to Ignore/IgnoreVector objects""" - if len(self.output_list) > 1: - return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(**calibration, **kwargs)])) - else: - return dict(zip(self.output_list, [misc.numeric_primitive(self.f(**calibration, **kwargs))])) - - # Currently does not use any of the machinery in SimpleBlock to deal with time displacements and hence - # can handle non-scalar inputs. - def steady_state(self, calibration, **kwargs): - return self._output_in_ss_format(calibration, **kwargs) diff --git a/sequence_jacobian/models/hank.py b/sequence_jacobian/models/hank.py index a5fb229..f181272 100644 --- a/sequence_jacobian/models/hank.py +++ b/sequence_jacobian/models/hank.py @@ -4,7 +4,6 @@ from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.het_block import het -from ..blocks.helper_block import helper '''Part 1: HA block''' @@ -161,7 +160,7 @@ def asset_state_vars(amax, nA): return a_grid -@helper +@simple def partial_steady_state_solution(B_Y, mu, r): B = B_Y w = 1 / mu diff --git a/sequence_jacobian/models/krusell_smith.py b/sequence_jacobian/models/krusell_smith.py index 8bc5950..ecdb0b9 100644 --- a/sequence_jacobian/models/krusell_smith.py +++ b/sequence_jacobian/models/krusell_smith.py @@ -4,7 +4,6 @@ from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.het_block import het -from ..blocks.helper_block import helper '''Part 1: HA block''' @@ -77,7 +76,7 @@ def asset_state_vars(amax, nA): return a_grid -@helper +@simple def firm_steady_state_solution(r, delta, alpha): rk = r + delta Z = (rk / alpha) ** alpha # normalize so that Y=1 diff --git a/sequence_jacobian/models/rbc.py b/sequence_jacobian/models/rbc.py index 7ff2b9b..7ff537b 100644 --- a/sequence_jacobian/models/rbc.py +++ b/sequence_jacobian/models/rbc.py @@ -1,7 +1,6 @@ import numpy as np from ..blocks.simple_block import simple -from ..blocks.helper_block import helper '''Part 1: Simple blocks''' @@ -29,7 +28,7 @@ def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): return goods_mkt, euler, walras -@helper +@simple def steady_state_solution(r, eis, delta, alpha): rk = r + delta Z = (rk / alpha) ** alpha # normalize so that Y=1 diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index ed811dd..884df5a 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -7,11 +7,10 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check from ..utilities import solvers, graph, misc -from ..blocks.helper_block import HelperBlock # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, +def steady_state(blocks, calibration, unknowns, targets, helper_indices=None, sort_blocks=True, consistency_check=True, ttol=2e-12, ctol=1e-9, block_kwargs=None, verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): @@ -19,21 +18,27 @@ def steady_state(blocks, calibration, unknowns, targets, For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. blocks: `list` - A list of blocks, which include the types: SimpleBlock, HetBlock, HelperBlock, SolvedBlock, CombinedBlock + A list of blocks, which include the types: SimpleBlock, HetBlock, SolvedBlock, CombinedBlock calibration: `dict` The pre-specified values of variables/parameters provided to the steady state computation unknowns: `dict` A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver targets: `dict` A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG + helper_indices: `list` + A list of indices (int) indicating which blocks in `blocks` are "helper blocks", i.e. SimpleBlocks that replace + some of the equations in the DAG for the purposes of aiding steady state calculation + sort_blocks: `bool` + Whether the blocks need to be topologically sorted (only False when this function is called from within a + Block object, like CombinedBlock, that has already pre-sorted the blocks) consistency_check: `bool` - If HelperBlocks are a portion of the argument blocks, re-run the DAG with the computed steady state values - without the assistance of HelperBlocks and see if the targets are still hit + If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values + without the assistance of helper blocks and see if the targets are still hit ttol: `float` The tolerance for the targets---how close the user wants the computed target values to equal the desired values ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the - use of HelperBlocks, to equal the desired values + use of helper blocks, to equal the desired values block_kwargs: `dict` A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that @@ -59,6 +64,8 @@ def steady_state(blocks, calibration, unknowns, targets, """ # Populate otherwise mutable default arguments + if helper_indices is None: + helper_indices = [] if block_kwargs is None: block_kwargs = {} if solver_kwargs is None: @@ -67,7 +74,11 @@ def steady_state(blocks, calibration, unknowns, targets, constrained_kwargs = {} ss_values = deepcopy(calibration) - topsorted = graph.block_sort(blocks, calibration=calibration) + if sort_blocks: + topsorted = graph.block_sort(blocks, ignore_helpers=False, + helper_indices=helper_indices, calibration=calibration) + else: + topsorted = range(len(blocks)) def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False): ss_values.update(misc.smart_zip(unknowns.keys(), unknown_values)) @@ -77,7 +88,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Progress through the DAG computing the resulting steady state values based on the unknown_values # provided to the residual function for i in topsorted: - if not include_helpers and isinstance(blocks[i], HelperBlock): + if not include_helpers and i in helper_indices: continue # Want to see hetoutputs elif hasattr(blocks[i], 'hetoutput') and blocks[i].hetoutput is not None: @@ -86,7 +97,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False else: outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) - if include_helpers and isinstance(blocks[i], HelperBlock): + if include_helpers and i in helper_indices: helper_outputs.update(outputs) ss_values.update(outputs) else: diff --git a/sequence_jacobian/utilities/graph.py b/sequence_jacobian/utilities/graph.py index 0ca62aa..bc541d4 100644 --- a/sequence_jacobian/utilities/graph.py +++ b/sequence_jacobian/utilities/graph.py @@ -1,63 +1,67 @@ """Topological sort and related code""" -from ..blocks.helper_block import HelperBlock - -def block_sort(block_list, ignore_helpers=False, calibration=None, return_io=False): +def block_sort(blocks, ignore_helpers=False, helper_indices=None, calibration=None, return_io=False): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's inferred) that indicate their aggregate inputs and outputs - Importantly, because including HelperBlocks in a block_list without additional measures + Importantly, because including helper blocks in a blocks without additional measures can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the steady_state computation to resolve these cycles. e.g. Consider Krusell Smith: - Suppose one specifies a HelperBlock based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the HelperBlock - because the "firm" block outputs "r", which the HelperBlock takes as an input. - However, it would also include the HelperBlock as a dependency of the "firm" block because the "firm" block takes + Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the helper block + because the "firm" block outputs "r", which the helper block takes as an input. + However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes "K" as an input. This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of HelperBlock and the cycle would be resolved. + "firm" could be removed as a dependency of helper block and the cycle would be resolved. - block_list: `list` - A list of the blocks (SimpleBlock, HetBlock, HelperBlock, etc.) to sort + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort ignore_helpers: `bool` - A boolean indicating whether to account for/return the indices of HelperBlocks contained in block_list + A boolean indicating whether to account for/return the indices of helper blocks contained in blocks Set to true when sorting for td and jac calculations + helper_indices: `list` + A list of indices corresponding to the helper blocks in the blocks calibration: `dict` or `None` An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using HelperBlocks. Read above docstring for more detail + introduced by using helper blocks. Read above docstring for more detail return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `block_list` + A boolean indicating whether to return the full set of input and output arguments from `blocks` """ + if helper_indices is None: + helper_indices = [] # TODO: Decide whether we want to break out the input and output argument tracking and return to # a different function... currently it's very convenient to slot it into block_sort directly, but it # does clutter up the function body if return_io: # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(block_list, return_output_args=True) + outmap, outargs = construct_output_map(blocks, ignore_helpers=ignore_helpers, + helper_indices=helper_indices, return_output_args=True) # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph(block_list, outmap, return_input_args=True, - calibration=calibration, ignore_helpers=ignore_helpers) + dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, + ignore_helpers=ignore_helpers, + helper_indices=helper_indices, calibration=calibration) if ignore_helpers: - return ignore_helper_block_indices(topological_sort(dep), block_list), inargs, outargs + return [i for i in topological_sort(dep) if i not in helper_indices], inargs, outargs else: return topological_sort(dep), inargs, outargs else: # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(block_list) + outmap = construct_output_map(blocks, ignore_helpers=ignore_helpers, helper_indices=helper_indices) # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(block_list, outmap, - calibration=calibration, ignore_helpers=ignore_helpers) + dep = construct_dependency_graph(blocks, outmap, calibration=calibration, + ignore_helpers=ignore_helpers, helper_indices=helper_indices) if ignore_helpers: - return ignore_helper_block_indices(topological_sort(dep), block_list) + return [i for i in topological_sort(dep) if i not in helper_indices] else: return topological_sort(dep) @@ -89,26 +93,25 @@ def topological_sort(dep, names=None): return topsorted -def ignore_helper_block_indices(topsorted, blocks): - return [i for i in topsorted if not isinstance(blocks[i], HelperBlock)] - - -def construct_output_map(block_list, ignore_helpers=False, return_output_args=False): +def construct_output_map(blocks, ignore_helpers=False, helper_indices=None, return_output_args=False): """Construct a map of outputs to the indices of the blocks that produce them. - block_list: `list` - A list of the blocks (SimpleBlock, HetBlock, HelperBlock, etc.) to sort + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, helper block, etc.) to sort ignore_helpers: `bool` - A boolean indicating whether to account for/return the indices of HelperBlocks contained in block_list + A boolean indicating whether to account for/return the indices of helper blocks contained in blocks Set to true when sorting for td and jac calculations return_output_args: `bool` A boolean indicating whether to track and return the full set of output arguments of all of the blocks - in `block_list` + in `blocks` """ + if helper_indices is None: + helper_indices = [] + outmap = dict() outargs = set() - for num, block in enumerate(block_list): - if ignore_helpers and isinstance(block, HelperBlock): + for num, block in enumerate(blocks): + if ignore_helpers and num in helper_indices: continue # Find the relevant set of outputs corresponding to a block @@ -120,15 +123,15 @@ def construct_output_map(block_list, ignore_helpers=False, return_output_args=Fa raise ValueError(f'{block} is not recognized as block or does not provide outputs') for o in outputs: - # Because some of the outputs of a HelperBlock are, by construction, outputs that also appear in the + # Because some of the outputs of a helper block are, by construction, outputs that also appear in the # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering # throwing this ValueError - if o in outmap and not (isinstance(block, HelperBlock) or isinstance(block_list[outmap[o]], HelperBlock)): + if o in outmap and not (num in helper_indices or outmap[o] in helper_indices): raise ValueError(f'{o} is output twice') - # Ensure that the block "outmap" maps "o" to is the actual block and not a HelperBlock if both share + # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap or (o in outmap and not isinstance(block, HelperBlock)): + if o not in outmap or (o in outmap and num not in helper_indices): outmap[o] = num if return_output_args: outargs.add(o) @@ -140,7 +143,8 @@ def construct_output_map(block_list, ignore_helpers=False, return_output_args=Fa return outmap -def construct_dependency_graph(block_list, outmap, ignore_helpers=False, calibration=None, return_input_args=False): +def construct_dependency_graph(blocks, outmap, ignore_helpers=False, helper_indices=None, + calibration=None, return_input_args=False): """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where this set is the set of blocks that the key block is dependent on. @@ -150,10 +154,13 @@ def construct_dependency_graph(block_list, outmap, ignore_helpers=False, calibra """ if calibration is None: calibration = {} - dep = {num: set() for num in range(len(block_list))} + if helper_indices is None: + helper_indices = [] + + dep = {num: set() for num in range(len(blocks))} inargs = set() - for num, block in enumerate(block_list): - if ignore_helpers and isinstance(block, HelperBlock): + for num, block in enumerate(blocks): + if ignore_helpers and num in helper_indices: continue if hasattr(block, 'inputs'): inputs = block.inputs @@ -165,12 +172,12 @@ def construct_dependency_graph(block_list, outmap, ignore_helpers=False, calibra # Each potential input to a given block will either be 1) output by another block, # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into # the steady-state computation via the `calibration' dict. - # If the block is a HelperBlock, then we want to check the calibration to see if the potential + # If the block is a helper block, then we want to check the calibration to see if the potential # input is a pre-specified variable/parameter, and if it is then we will not add the block that # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution HelperBlock and firm block would create a cyclic + # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and isinstance(block, HelperBlock)): + if i in outmap and not (i in calibration and num in helper_indices): dep[num].add(outmap[i]) if return_input_args: return dep, inargs @@ -178,16 +185,19 @@ def construct_dependency_graph(block_list, outmap, ignore_helpers=False, calibra return dep -def find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=False): - """Find outputs of the blocks in block_list that are inputs to other blocks in block_list. +def find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=False, helper_indices=None): + """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. See the docstring of construct_output_map for more details about the arguments. """ + if helper_indices is None: + helper_indices = [] + required = set() - outmap = construct_output_map(block_list, ignore_helpers=ignore_helpers) - for block in block_list: - if ignore_helpers and isinstance(block, HelperBlock): + outmap = construct_output_map(blocks, ignore_helpers=ignore_helpers, helper_indices=helper_indices) + for num, block in enumerate(blocks): + if ignore_helpers and num in helper_indices: continue if hasattr(block, 'inputs'): inputs = block.inputs From f914d46bcac7364a1f48a9f246e08f4f39783abd Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 17 Mar 2021 15:00:02 -0500 Subject: [PATCH 082/288] Update tests to comply with the new helper_indices convention --- sequence_jacobian/models/two_asset.py | 5 +- tests/base/test_estimation.py | 7 +-- tests/base/test_jacobian.py | 34 +++++------ tests/base/test_public_classes.py | 10 ++- tests/base/test_steady_state.py | 16 ++--- tests/base/test_transitional_dynamics.py | 77 +++++++++++------------- tests/conftest.py | 67 +++++++++++---------- tests/robustness/test_steady_state.py | 22 +++---- 8 files changed, 111 insertions(+), 127 deletions(-) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 352f5b3..04d9e3a 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -5,9 +5,8 @@ from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.het_block import het, hetoutput -from ..blocks.helper_block import helper from ..blocks.solved_block import solved -from ..blocks.support.simple_displacement import apply_function, Displace +from ..blocks.support.simple_displacement import apply_function '''Part 1: HA block''' @@ -303,7 +302,7 @@ def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): return b_grid, a_grid, k_grid, e_grid, Pi -@helper +@simple def partial_steady_state_solution(delta, K, r, tot_wealth, Bh, Bg, G, omega): I = delta * K mc = 1 - r * (tot_wealth - Bg - K) diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 14e0bd1..97a2903 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -8,13 +8,12 @@ # See test_determinacy.py for the to-do describing this suppression @pytest.mark.filterwarnings("ignore:.*cannot be safely interpreted as an integer.*:DeprecationWarning") -def test_krusell_smith_estimation(krusell_smith_model): - blocks, exogenous, unknowns, targets, ss = krusell_smith_model +def test_krusell_smith_estimation(krusell_smith_dag): + ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag np.random.seed(41234) T = 50 - G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) # Step 1: Stacked impulse responses rho = 0.9 diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 5358825..7404d46 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -6,14 +6,13 @@ from sequence_jacobian.jacobian.classes import JacobianDict -def test_ks_jac(krusell_smith_model): - blocks, exogenous, unknowns, targets, ss = krusell_smith_model - household, firm, mkt_clearing, _, _, _ = blocks +def test_ks_jac(krusell_smith_dag): + ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag + household, firm, mkt_clearing, _, _, _ = ks_model._blocks_unsorted T = 10 # Automatically calculate the general equilibrium Jacobian - G2 = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G2 = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) # Manually calculate the general equilibrium Jacobian J_firm = firm.jac(ss, shock_list=['K', 'Z']) @@ -35,16 +34,15 @@ def test_ks_jac(krusell_smith_model): assert np.allclose(G2[o]['Z'], G[o]) -def test_hank_jac(one_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = one_asset_hank_model +def test_hank_jac(one_asset_hank_dag): + hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag T = 10 # Automatically calculate the general equilibrium Jacobian - G2 = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G2 = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) # Manually calculate the general equilibrium Jacobian - curlyJs, required = curlyJ_sorted(blocks, unknowns+exogenous, ss, T) + curlyJs, required = curlyJ_sorted(hank_model.blocks, unknowns + exogenous, ss, T) J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) H_U = J_curlyH_U[targets, unknowns].pack(T) @@ -59,12 +57,12 @@ def test_hank_jac(one_asset_hank_model): assert np.allclose(G[o][i], G2[o][i]) -def test_fake_news_v_actual(one_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = one_asset_hank_model +def test_fake_news_v_actual(one_asset_hank_dag): + hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag - household = blocks[0] + household = hank_model._blocks_unsorted[0] T = 40 - shock_list=['w', 'r', 'Div', 'Tax'] + shock_list = ['w', 'r', 'Div', 'Tax'] Js = household.jac(ss, shock_list, T) output_list = household.non_back_iter_outputs @@ -80,7 +78,7 @@ def test_fake_news_v_actual(one_asset_hank_model): sspol_pi, sspol_space, T, h, ss_for_hetinput) asset_effects = np.sum(curlyDs['r'] * ss['a_grid'], axis=(1, 2)) - assert np.linalg.norm(asset_effects - curlyYs["r"]["a"], np.inf) < 1e-15 + assert np.linalg.norm(asset_effects - curlyYs["r"]["a"], np.inf) < 2e-15 # Step 2 of fake news algorithm: (transpose) forward iteration curlyPs = {} @@ -122,10 +120,10 @@ def test_fake_news_v_actual(one_asset_hank_model): assert np.array_equal(Js[o.capitalize()][i], Js_original[o.capitalize()][i]) -def test_fake_news_v_direct_method(one_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = one_asset_hank_model +def test_fake_news_v_direct_method(one_asset_hank_dag): + hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag - household = blocks[0] + household = hank_model._blocks_unsorted[0] T = 40 shock_list = 'r' output_list = household.non_back_iter_outputs diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 84b221a..5a048a8 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -5,15 +5,13 @@ from sequence_jacobian.blocks.support.impulse import ImpulseDict -def test_impulsedict(krusell_smith_model): - blocks, exogenous, unknowns, targets, ss = krusell_smith_model - household, firm, mkt_clearing, _, _, _ = blocks +def test_impulsedict(krusell_smith_dag): + ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 200 # Linearized impulse responses as deviations, nonlinear as levels - ks = create_model(*blocks, name='KS') - ir_lin = ks.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) - ir_nonlin = ks.solve_impulse_nonlinear(ss, {'Z': 0.01 * 0.5 ** np.arange(T)}, unknowns, targets) + ir_lin = ks_model.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) + ir_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.01 * 0.5 ** np.arange(T)}, unknowns, targets) # Get method assert isinstance(ir_lin, ImpulseDict) diff --git a/tests/base/test_steady_state.py b/tests/base/test_steady_state.py index dbc979c..0a4c372 100644 --- a/tests/base/test_steady_state.py +++ b/tests/base/test_steady_state.py @@ -5,32 +5,32 @@ from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset -def test_rbc_steady_state(rbc_model): - _, _, _, _, ss = rbc_model +def test_rbc_steady_state(rbc_dag): + _, _, _, _, ss = rbc_dag ss_ref = rbc.rbc_ss() assert set(ss.keys()) == set(ss_ref.keys()) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_ks_steady_state(krusell_smith_model): - _, _, _, _, ss = krusell_smith_model +def test_ks_steady_state(krusell_smith_dag): + _, _, _, _, ss = krusell_smith_dag ss_ref = krusell_smith.ks_ss(nS=2, nA=10, amax=200) assert set(ss.keys()) == set(ss_ref.keys()) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_hank_steady_state(one_asset_hank_model): - _, _, _, _, ss = one_asset_hank_model +def test_hank_steady_state(one_asset_hank_dag): + _, _, _, _, ss = one_asset_hank_dag ss_ref = hank.hank_ss(nS=2, nA=10, amax=150) assert set(ss.keys()) == set(ss_ref.keys()) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_two_asset_steady_state(two_asset_hank_model): - _, _, _, _, ss = two_asset_hank_model +def test_two_asset_steady_state(two_asset_hank_dag): + _, _, _, _, ss = two_asset_hank_dag ss_ref = two_asset.two_asset_ss(nZ=3, nB=10, nA=16, nK=4, verbose=False) assert set(ss.keys()) == set(ss_ref.keys()) for k in ss.keys(): diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 3ffc982..a011017 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -3,28 +3,26 @@ import numpy as np import copy -from sequence_jacobian import two_asset, nonlinear, get_G, get_H_U +from sequence_jacobian import two_asset, combine, get_H_U from sequence_jacobian import utilities as utils # TODO: Figure out a more robust way to check similarity of the linear and non-linear solution. # As of now just checking that the tolerance for difference (by infinity norm) is below a manually checked threshold -def test_rbc_td(rbc_model): - blocks, exogenous, unknowns, targets, ss = rbc_model +def test_rbc_td(rbc_dag): + rbc_model, exogenous, unknowns, targets, ss = rbc_dag T, impact, rho, news = 30, 0.01, 0.8, 10 - G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = rbc_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) dZ = np.empty((T, 2)) dZ[:, 0] = impact * ss['Z'] * rho**np.arange(T) dZ[:, 1] = np.concatenate((np.zeros(news), dZ[:-news, 0])) dC = 100 * G['C']['Z'] @ dZ / ss['C'] - td_nonlin = nonlinear.td_solve(block_list=blocks, ss=ss, exogenous={"Z": ss["Z"] + dZ[:, 0]}, - unknowns=unknowns, targets=targets, verbose=False) - td_nonlin_news = nonlinear.td_solve(block_list=blocks, ss=ss, exogenous={"Z": ss["Z"] + dZ[:, 1]}, - unknowns=unknowns, targets=targets, verbose=False) + td_nonlin = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 0]}, unknowns=unknowns, targets=targets) + td_nonlin_news = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 1]}, unknowns=unknowns, targets=targets) + dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_nonlin_news = 100 * (td_nonlin_news['C'] / ss['C'] - 1) @@ -32,40 +30,37 @@ def test_rbc_td(rbc_model): assert np.linalg.norm(dC[:, 1] - dC_nonlin_news, np.inf) < 7e-2 -def test_ks_td(krusell_smith_model): - blocks, exogenous, unknowns, targets, ss = krusell_smith_model +def test_ks_td(krusell_smith_dag): + ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 30 - G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss) + G = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) for shock_size, tol in [(0.01, 7e-3), (0.1, 0.6)]: - Z = ss["Z"] + shock_size * 0.8 ** np.arange(T) + dZ = shock_size * 0.8 ** np.arange(T) - td_nonlin = nonlinear.td_solve(ss=ss, block_list=blocks, exogenous={"Z": Z}, unknowns=unknowns, - targets=targets, monotonic=True, verbose=False) + td_nonlin = ks_model.solve_impulse_nonlinear(ss, {"Z": dZ}, unknowns=unknowns, targets=targets, + monotonic=True) dr_nonlin = 10000 * (td_nonlin['r'] - ss['r']) - dr_lin = 10000 * G['r']['Z'] @ (Z - ss['Z']) + dr_lin = 10000 * G['r']['Z'] @ dZ assert np.linalg.norm(dr_nonlin - dr_lin, np.inf) < tol -def test_hank_td(one_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = one_asset_hank_model +def test_hank_td(one_asset_hank_dag): + hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag T = 30 - G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss, save=True) + G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, save=True) rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) - rstar = ss['r'] + drstar - H_U = get_H_U(blocks, unknowns, targets, T, ss, use_saved=True) + H_U = get_H_U(hank_model.blocks, unknowns, targets, T, ss, use_saved=True) H_U_factored = utils.misc.factor(H_U) - td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": rstar}, - unknowns=unknowns, targets=targets, H_U_factored=H_U_factored, verbose=False) + td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, + H_U_factored=H_U_factored) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -73,28 +68,25 @@ def test_hank_td(one_asset_hank_model): assert np.linalg.norm(dC_nonlin - dC_lin, np.inf) < 3e-3 -def test_two_asset_td(two_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = two_asset_hank_model +def test_two_asset_td(two_asset_hank_dag): + two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag T = 30 - G = get_G(block_list=blocks, exogenous=exogenous, unknowns=unknowns, - targets=targets, T=T, ss=ss, save=True) + G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, save=True) for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: - drstar = -0.0025 * 0.6 ** np.arange(T) - rstar = ss["r"] + shock_size * drstar + drstar = shock_size * -0.0025 * 0.6 ** np.arange(T) - td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": rstar}, - unknowns=unknowns, targets=targets, use_saved=True, verbose=False) + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, use_saved=True) dY_nonlin = 100 * (td_nonlin['Y'] - 1) - dY_lin = shock_size * 100 * G['Y']['rstar'] @ drstar + dY_lin = 100 * G['Y']['rstar'] @ drstar assert np.linalg.norm(dY_nonlin - dY_lin, np.inf) < tol -def test_two_asset_solved_v_simple_td(two_asset_hank_model): - blocks, exogenous, unknowns, targets, ss = two_asset_hank_model +def test_two_asset_solved_v_simple_td(two_asset_hank_dag): + two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag household = copy.deepcopy(two_asset.household) household.add_hetoutput(two_asset.adjustment_costs, verbose=False) @@ -103,24 +95,23 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_model): two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing, two_asset.partial_steady_state_solution] + two_asset_model_simple = combine(*blocks_simple, name="Two-Asset HANK w/ SimpleBlocks", helper_indices=[13]) unknowns_simple = ["r", "w", "Y", "pi", "p", "Q", "K"] targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] T = 30 - G = get_G(blocks, exogenous, unknowns, targets, T, ss=ss, save=True) - G_simple = get_G(blocks_simple, exogenous, unknowns_simple, targets_simple, T, ss=ss, save=True) + G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, save=True) + G_simple = two_asset_model_simple.solve_jacobian(ss, exogenous, unknowns_simple, targets_simple, T=T, save=True) drstar = -0.0025 * 0.6 ** np.arange(T) dY = 100 * G['Y']['rstar'] @ drstar - td_nonlin = nonlinear.td_solve(blocks, ss, exogenous={"rstar": ss["r"] + drstar}, - unknowns=unknowns, targets=targets, use_saved=True, verbose=False) + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, use_saved=True) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_simple = 100 * G_simple['Y']['rstar'] @ drstar - td_nonlin_simple = nonlinear.td_solve(blocks_simple, ss, exogenous={"rstar": ss["r"] + drstar}, - unknowns=unknowns_simple, targets=targets_simple, - use_saved=True, verbose=False) + td_nonlin_simple = two_asset_model_simple.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns_simple, + targets_simple, use_saved=True) dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) diff --git a/tests/conftest.py b/tests/conftest.py index e4ff791..b8b1575 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,73 +3,74 @@ import pytest import copy -from sequence_jacobian import steady_state +from sequence_jacobian import create_model from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset @pytest.fixture(scope='session') -def rbc_model(): +def rbc_dag(): blocks = [rbc.household, rbc.mkt_clearing, rbc.firm, rbc.steady_state_solution] + rbc_model = create_model(*blocks, name="RBC", helper_indices=[3]) # Steady State calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "frisch": 1., "L": 1.0, "r": 0.01} - ss_unknowns = {"beta": None, "vphi": None} - ss_targets = {"goods_mkt": 0, "euler": 0} - ss = steady_state(blocks, calibration, ss_unknowns, ss_targets, solver="solved", consistency_check=True) + unknowns_ss = {"beta": None, "vphi": None} + targets_ss = {"goods_mkt": 0, "euler": 0} + ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="solved") # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] - dynamic_unknowns = ["K", "L"] - dynamic_targets = ["goods_mkt", "euler"] + unknowns = ["K", "L"] + targets = ["goods_mkt", "euler"] - return blocks, exogenous, dynamic_unknowns, dynamic_targets, ss + return rbc_model, exogenous, unknowns, targets, ss @pytest.fixture(scope='session') -def krusell_smith_model(): +def krusell_smith_dag(): blocks = [krusell_smith.household, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars, krusell_smith.asset_state_vars, krusell_smith.firm_steady_state_solution] + ks_model = create_model(*blocks, name="Krusell-Smith", helper_indices=[5]) # Steady State calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01} - ss_unknowns = {"beta": (0.98/1.01, 0.999/1.01)} - ss_targets = {"K": "A"} - ss = steady_state(blocks, calibration, ss_unknowns, ss_targets, - solver="brentq", consistency_check=True) + unknowns_ss = {"beta": (0.98/1.01, 0.999/1.01)} + targets_ss = {"K": "A"} + ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq") # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] - dynamic_unknowns = ["K"] - dynamic_targets = ["asset_mkt"] + unknowns = ["K"] + targets = ["asset_mkt"] - return blocks, exogenous, dynamic_unknowns, dynamic_targets, ss + return ks_model, exogenous, unknowns, targets, ss @pytest.fixture(scope='session') -def one_asset_hank_model(): +def one_asset_hank_dag(): blocks = [hank.household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc, hank.income_state_vars, hank.asset_state_vars, hank.partial_steady_state_solution] + hank_model = create_model(*blocks, name="One-Asset HANK", helper_indices=[8]) # Steady State calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} - ss_unknowns = {"beta": 0.986, "vphi": 0.8} - ss_targets = {"asset_mkt": 0, "labor_mkt": 0} - ss = steady_state(blocks, calibration, ss_unknowns, ss_targets, - solver="broyden_custom", consistency_check=True, verbose=False) + unknowns_ss = {"beta": 0.986, "vphi": 0.8} + targets_ss = {"asset_mkt": 0, "labor_mkt": 0} + ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom") # Transitional Dynamics/Jacobian Calculation - exogenous= ["rstar", "Z"] - dynamic_unknowns = ["pi", "w", "Y"] - dynamic_targets = ["nkpc_res", "asset_mkt", "labor_mkt"] + exogenous = ["rstar", "Z"] + unknowns = ["pi", "w", "Y"] + targets = ["nkpc_res", "asset_mkt", "labor_mkt"] - return blocks, exogenous, dynamic_unknowns, dynamic_targets, ss + return hank_model, exogenous, unknowns, targets, ss @pytest.fixture(scope='session') -def two_asset_hank_model(): +def two_asset_hank_dag(): household = copy.deepcopy(two_asset.household) household.add_hetoutput(two_asset.adjustment_costs, verbose=False) blocks = [household, two_asset.make_grids, @@ -77,6 +78,7 @@ def two_asset_hank_model(): two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing, two_asset.partial_steady_state_solution] + two_asset_model = create_model(*blocks, name="Two-Asset HANK", helper_indices=[12]) # Steady State calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, @@ -84,14 +86,13 @@ def two_asset_hank_model(): "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - ss_unknowns = {"beta": 0.976, "vphi": 2.07, "chi1": 6.5} - ss_targets = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} - ss = steady_state(blocks, calibration, ss_unknowns, ss_targets, - solver="broyden_custom", consistency_check=True, verbose=False) + unknowns_ss = {"beta": 0.976, "vphi": 2.07, "chi1": 6.5} + targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} + ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom") # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z", "G"] - dynamic_unknowns = ["r", "w", "Y"] - dynamic_targets = ["asset_mkt", "fisher", "wnkpc"] + unknowns = ["r", "w", "Y"] + targets = ["asset_mkt", "fisher", "wnkpc"] - return blocks, exogenous, dynamic_unknowns, dynamic_targets, ss + return two_asset_model, exogenous, unknowns, targets, ss diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index 37a662e..c28225d 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -1,30 +1,28 @@ import pytest import numpy as np -import sequence_jacobian as sj - # Filter out warnings when the solver is trying to search in bad regions @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") -def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_model): - blocks, _, _, _, ss = one_asset_hank_model +def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): + hank_model, _, _, _, ss = one_asset_hank_dag calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} unknowns = {"beta": (0.95, 0.97, 0.999/(1 + 0.005)), "vphi": (0.001, 1.0, 10)} targets = {"asset_mkt": 0, "labor_mkt": 0} - ss_ref = sj.steady_state(blocks, calibration, unknowns, targets, - solver="broyden1", consistency_check=True, verbose=False, - solver_kwargs={"options": {"maxiter": 250}}, - constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) + ss_ref = hank_model.solve_steady_state(calibration, unknowns, targets, + solver="broyden1", solver_kwargs={"options": {"maxiter": 250}}, + constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) + for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") -def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_model): - blocks, _, _, _, ss = two_asset_hank_model +def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag): + two_asset_model, _, _, _, ss = two_asset_hank_dag calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, "tot_wealth": 14, "K": 10, "delta": 0.02, "kappap": 0.1, "muw": 1.1, "Bh": 1.04, @@ -33,7 +31,7 @@ def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_mod "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns = {"beta": (0.5, 0.9, 0.999 / (1 + 0.0125)), "vphi": (0.001, 1.0, 10.), "chi1": (0.5, 5.5, 10.)} targets = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} - ss_ref = sj.steady_state(blocks, calibration, unknowns, targets, - solver="broyden_custom", consistency_check=True, verbose=False) + ss_ref = two_asset_model.solve_steady_state(calibration, unknowns, targets, solver="broyden_custom", + consistency_check=True) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) From 98e94b051db019cf091183256970bde0252d54f3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 17 Mar 2021 16:29:55 -0500 Subject: [PATCH 083/288] Implement JacobianDictBlock --- .../blocks/auxiliary_blocks/__init__.py | 1 + .../auxiliary_blocks/jacobiandict_block.py | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 sequence_jacobian/blocks/auxiliary_blocks/__init__.py create mode 100644 sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py diff --git a/sequence_jacobian/blocks/auxiliary_blocks/__init__.py b/sequence_jacobian/blocks/auxiliary_blocks/__init__.py new file mode 100644 index 0000000..d4a7666 --- /dev/null +++ b/sequence_jacobian/blocks/auxiliary_blocks/__init__.py @@ -0,0 +1 @@ +"""Auxiliary Block types for building a coherent backend for Block handling""" diff --git a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py new file mode 100644 index 0000000..cc945c9 --- /dev/null +++ b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -0,0 +1,37 @@ +"""A simple wrapper for JacobianDicts to be embedded in DAGs""" + +from numbers import Real +from typing import Dict, Union, List + +from ...primitives import Block, Array +from ...jacobian.classes import JacobianDict + + +class JacobianDictBlock(JacobianDict, Block): + """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" + def __init__(self, nesteddict, outputs=None, inputs=None): + super().__init__(nesteddict, outputs=outputs, inputs=inputs) + + def __repr__(self): + return f"" + + def impulse_linear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: + return self.jacobian(list(exogenous.keys())).apply(exogenous) + + def jacobian(self, exogenous: List[str] = None, **kwargs) -> JacobianDict: + if exogenous is None: + return JacobianDict(self.nesteddict) + else: + if self.inputs == exogenous: + return JacobianDict(self.nesteddict) + else: + nesteddict_subset = {} + for o in self.nesteddict.keys(): + for i in self.nesteddict[o].keys(): + if i in exogenous: + if o in nesteddict_subset: + nesteddict_subset[o][i] = self.nesteddict[o][i] + else: + nesteddict_subset[o] = {i: self.nesteddict[o][i]} + return JacobianDict(nesteddict_subset) From fe7e42ae1d7aa625e15b5bba185cb9e04eda19ad Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 29 Mar 2021 14:55:06 -0500 Subject: [PATCH 084/288] Fixed two small mistakes in two-asset HANK: (i) way that muw enters NKWPC, (ii) accounting for the resource cost of capital adjustment. These have negligible effects on results, because vphi scales to offset (i) and (ii) is 2nd order. --- sequence_jacobian/models/two_asset.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 352f5b3..da8207d 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -237,9 +237,10 @@ def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): @simple -def dividend(Y, w, N, K, pi, mup, kappap, delta): +def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y - I = K - (1 - delta) * K(-1) + k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) + I = K - (1 - delta) * K(-1) + k_adjust div = Y - w * N - I - psip return psip, I, div @@ -267,13 +268,12 @@ def finance(i, p, pi, r, div, omega, pshare): @simple def wage(pi, w, N, muw, kappaw): piw = (1 + pi) * w / w(-1) - 1 - psiw = muw / (1 - muw) / 2 / kappaw * (1 + piw).apply(np.log) ** 2 * N - return piw, psiw + return piw @simple def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N**(1+1/frisch) - muw*(1-tax)*w*N*U) + beta *\ + wnkpc = kappaw * (vphi * N**(1+1/frisch) - (1-tax)*w*N*U/muw) + beta *\ (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) return wnkpc @@ -281,15 +281,15 @@ def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): @simple def mkt_clearing(p, A, B, Bg, vphi, muw, tax, w, U): asset_mkt = p + Bg - B - A - labor_mkt = vphi - muw * (1 - tax) * w * U + labor_mkt = vphi - (1 - tax) * w * U / muw return asset_mkt, labor_mkt @simple def mkt_clearing_all(p, A, B, Bg, vphi, muw, tax, w, U, C, I, G, Chi, omega): asset_mkt = p + Bg - B - A - labor_mkt = vphi - muw * (1 - tax) * w * U - goods_mkt = C + I + G + Chi + omega * B - 1 + labor_mkt = vphi - (1 - tax) * w * U / muw + goods_mkt = C + I + G + Chi + psip + omega * B - 1 return asset_mkt, labor_mkt, goods_mkt @@ -363,7 +363,7 @@ def res(x): out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) asset_mkt = out['A'] + out['B'] - p - Bg - labor_mkt = vphi_loc - muw * (1 - tax) * w * out['U'] + labor_mkt = vphi_loc - (1 - tax) * w * out['U'] / muw return np.array([asset_mkt, labor_mkt, out['B'] - Bh]) # solve for beta, vphi, omega @@ -388,11 +388,11 @@ def res(x): 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'z_grid': z_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, - 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psiw': 0, 'psip': 0, 'inv': 0, - 'labor_mkt': vphi - muw * (1 - tax) * w * ss["U"], + 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, + 'labor_mkt': vphi - (1 - tax) * w * ss["U"] / muw, 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1/mup), - 'wnkpc': kappaw * (vphi * ss["N"]**(1+1/frisch) - muw*(1-tax)*w*ss["N"]*ss["U"]), + 'wnkpc': kappaw * (vphi * ss["N"]**(1+1/frisch) - (1-tax)*w*ss["N"]*ss["U"]/muw), 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1-alpha) * mc - delta - r}) return ss From 44c5b575f29f3246104a93abd5ba23cae9eca987 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 30 Mar 2021 11:20:24 -0500 Subject: [PATCH 085/288] Implement LinearOperator abstract base class for valid Jacobian types and add `name` attribute to NestedDict and other Block types --- .../auxiliary_blocks/jacobiandict_block.py | 4 +- sequence_jacobian/blocks/het_block.py | 2 +- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/jacobian/classes.py | 49 +++++++++++++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index cc945c9..afcdc85 100644 --- a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -9,8 +9,8 @@ class JacobianDictBlock(JacobianDict, Block): """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" - def __init__(self, nesteddict, outputs=None, inputs=None): - super().__init__(nesteddict, outputs=outputs, inputs=inputs) + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) def __repr__(self): return f"" diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 3a5bd9a..b477ae2 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -449,7 +449,7 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= self.saved_shock_list, self.saved_output_list = relevant_shocks, outputs self.saved = {'curlyYs': curlyYs, 'curlyDs': curlyDs, 'curlyPs': curlyPs, 'F': F, 'J': J} - return JacobianDict(J) + return JacobianDict(J, name=self.name) def add_hetinput(self, hetinput, overwrite=False, verbose=True): """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index dc86503..f4b05af 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -163,7 +163,7 @@ def jacobian(self, ss, exogenous=None, T=None): if not J[o]: del J[o] - return JacobianDict(J) + return JacobianDict(J, name=self.name) def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 726ee1e..11b3935 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -1,12 +1,22 @@ """Various classes to support the computation of Jacobians""" +from abc import ABCMeta import copy import numpy as np from . import support -class IdentityMatrix: +class LinearOperator(metaclass=ABCMeta): + """An abstract base class encompassing all valid types representing Jacobians, which include + np.ndarray, IdentityMatrix, ZeroMatrix, and SimpleSparse.""" + pass + +# Make np.ndarray a child class of LinearOperator +LinearOperator.register(np.ndarray) + + +class IdentityMatrix(LinearOperator): """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, use to initialize Jacobian of a variable wrt itself""" __array_priority__ = 10_000 @@ -53,7 +63,7 @@ def __repr__(self): return 'IdentityMatrix' -class ZeroMatrix: +class ZeroMatrix(LinearOperator): """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, use in common case where some outputs don't depend on inputs""" __array_priority__ = 10_000 @@ -102,7 +112,7 @@ def __repr__(self): return 'ZeroMatrix' -class SimpleSparse: +class SimpleSparse(LinearOperator): """Efficient representation of sparse linear operators, which are linear combinations of basis operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s (measured by # above main diagonal) and m is number of initial entries missing. @@ -262,12 +272,14 @@ def __eq__(self, s): class NestedDict: - def __init__(self, nesteddict, outputs=None, inputs=None): + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): if isinstance(nesteddict, NestedDict): self.nesteddict = nesteddict.nesteddict self.outputs = nesteddict.outputs self.inputs = nesteddict.inputs + self.name = nesteddict.name else: + ensure_valid_nesteddict(nesteddict) self.nesteddict = nesteddict if outputs is None: outputs = list(nesteddict.keys()) @@ -279,6 +291,11 @@ def __init__(self, nesteddict, outputs=None, inputs=None): self.outputs = list(outputs) self.inputs = list(inputs) + if name is None: + # TODO: Figure out better default naming scheme for NestedDicts + self.name = "NestedDict" + else: + self.name = name def __repr__(self): return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' @@ -349,6 +366,30 @@ def deduplicate(mylist): return list(dict.fromkeys(mylist)) +def ensure_valid_nesteddict(d): + """The valid structure of `d` is a Dict[str, Dict[str, LinearOperator]], where calling `d[o][i]` yields a + Jacobian of type LinearOperator mapping sequences of `i` to sequences of `o`.""" + # Assuming it's sufficient to just check one of the keys and that someone won't be using multiple different types + if not isinstance(next(iter(d.keys())), str): + raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") + + jac_o_dict = next(iter(d.values())) + if isinstance(jac_o_dict, dict): + if not isinstance(next(iter(jac_o_dict.keys())), str): + raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" + f" `input` names.") + jac_o_i = next(iter(jac_o_dict.values())) + if not isinstance(jac_o_i, LinearOperator): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `LinearOperator`.") + else: + if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: + raise ValueError(f"The Jacobians in {d} must be square matrices of type `LinearOperator`.") + else: + raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" + f" values of type `LinearOperator`.") + + + class JacobianDict(NestedDict): @staticmethod def identity(ks): From fd34c62afbeb420c126c130cd1c9ea4a72919fd1 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 30 Mar 2021 11:21:51 -0500 Subject: [PATCH 086/288] Remove aliases module, shift aliases into combined_block module, and implement type conversion of nested dicts into JacobianDictBlock --- sequence_jacobian/__init__.py | 4 +--- sequence_jacobian/aliases.py | 10 ---------- sequence_jacobian/blocks/combined_block.py | 12 +++++++++++- 3 files changed, 12 insertions(+), 14 deletions(-) delete mode 100644 sequence_jacobian/aliases.py diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py index 57c538d..2f7abb6 100644 --- a/sequence_jacobian/__init__.py +++ b/sequence_jacobian/__init__.py @@ -4,12 +4,10 @@ from .models import rbc, krusell_smith, hank, two_asset -from .aliases import create_model - from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput from .blocks.solved_block import solved -from .blocks.combined_block import combine +from .blocks.combined_block import combine, create_model from .blocks.support.simple_displacement import apply_function from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved diff --git a/sequence_jacobian/aliases.py b/sequence_jacobian/aliases.py deleted file mode 100644 index aedcfe6..0000000 --- a/sequence_jacobian/aliases.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Type aliases, custom functions and errors for the base-level functionality of the package""" - -from .blocks.combined_block import CombinedBlock, combine - -# Useful type aliases -Model = CombinedBlock - -# Useful functional aliases -def create_model(*args, **kwargs): - return combine(*args, model_alias=True, **kwargs) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 8a41ac8..cc4ccfe 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -5,6 +5,7 @@ from .support.impulse import ImpulseDict from ..primitives import Block from .. import utilities as utils +from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict @@ -15,6 +16,11 @@ def combine(*args, name="", helper_indices=None, model_alias=False): return CombinedBlock(*args, name=name, helper_indices=helper_indices, model_alias=model_alias) +# Useful functional alias +def create_model(*args, **kwargs): + return combine(*args, model_alias=True, **kwargs) + + class CombinedBlock(Block): """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, @@ -25,7 +31,7 @@ class CombinedBlock(Block): def __init__(self, *blocks, name="", helper_indices=None, model_alias=False): # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. - self._blocks_unsorted = blocks + self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] # Upon instantiation, we only have enough information to conduct a sort ignoring helper blocks # since we need a `calibration` to resolve cyclic dependencies when including helper blocks in a topological sort @@ -138,3 +144,7 @@ def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwar solver = provide_solver_default(unknowns) return super().solve_steady_state(calibration, unknowns, targets, solver=solver, sort_blocks=False, **kwargs) + + +# Useful type aliases +Model = CombinedBlock From e761f2d8b99ab4ae9cedb9671695ed8f5dc38739 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 30 Mar 2021 11:28:48 -0500 Subject: [PATCH 087/288] Make {} a valid (empty) nested dict type for the purposes of using `ensure_valid_nesteddict` --- sequence_jacobian/jacobian/classes.py | 39 ++++++++++++++------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 11b3935..9b677cb 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -368,25 +368,28 @@ def deduplicate(mylist): def ensure_valid_nesteddict(d): """The valid structure of `d` is a Dict[str, Dict[str, LinearOperator]], where calling `d[o][i]` yields a - Jacobian of type LinearOperator mapping sequences of `i` to sequences of `o`.""" - # Assuming it's sufficient to just check one of the keys and that someone won't be using multiple different types - if not isinstance(next(iter(d.keys())), str): - raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") - - jac_o_dict = next(iter(d.values())) - if isinstance(jac_o_dict, dict): - if not isinstance(next(iter(jac_o_dict.keys())), str): - raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" - f" `input` names.") - jac_o_i = next(iter(jac_o_dict.values())) - if not isinstance(jac_o_i, LinearOperator): - raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `LinearOperator`.") + Jacobian of type LinearOperator mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed + to be {}, which is permitted the empty version of a valid nested dict.""" + + if d != {}: + # Assume it's sufficient to just check one of the keys + if not isinstance(next(iter(d.keys())), str): + raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") + + jac_o_dict = next(iter(d.values())) + if isinstance(jac_o_dict, dict): + if not isinstance(next(iter(jac_o_dict.keys())), str): + raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" + f" `input` names.") + jac_o_i = next(iter(jac_o_dict.values())) + if not isinstance(jac_o_i, LinearOperator): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `LinearOperator`.") + else: + if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: + raise ValueError(f"The Jacobians in {d} must be square matrices of type `LinearOperator`.") else: - if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: - raise ValueError(f"The Jacobians in {d} must be square matrices of type `LinearOperator`.") - else: - raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" - f" values of type `LinearOperator`.") + raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" + f" values of type `LinearOperator`.") From b21e1532165b637ac785254497298a6c7004bfb5 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 30 Mar 2021 11:42:24 -0500 Subject: [PATCH 088/288] Add JacobianDictBlock tests --- tests/base/test_jacobian_dict_block.py | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/base/test_jacobian_dict_block.py diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py new file mode 100644 index 0000000..992443c --- /dev/null +++ b/tests/base/test_jacobian_dict_block.py @@ -0,0 +1,43 @@ +"""Test JacobianDictBlock functionality""" + +import numpy as np + +from sequence_jacobian import combine +from sequence_jacobian.models import rbc +from sequence_jacobian.blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock + + +def test_jacobian_dict_block_impulses(rbc_dag): + rbc_model, exogenous, unknowns, _, ss = rbc_dag + + T = 10 + J_pe = rbc_model.jacobian(ss, exogenous=unknowns + exogenous, T=10) + J_block = JacobianDictBlock(J_pe) + + J_block_Z = J_block.jacobian(["Z"]) + for o in J_block_Z.outputs: + assert np.all(J_block[o]["Z"] == J_block_Z[o]["Z"]) + + dZ = 0.8 ** np.arange(T) + + dO1 = J_block @ {"Z": dZ} + dO2 = J_block_Z @ {"Z": dZ} + + for k in J_block: + assert np.all(dO1[k] == dO2[k]) + + +def test_jacobian_dict_block_combine(rbc_dag): + rbc_model, exogenous, unknowns, _, ss = rbc_dag + + J_firm = rbc.firm.jacobian(ss, exogenous=exogenous) + blocks_w_jdict = [rbc.household, J_firm, rbc.mkt_clearing] + cblock_w_jdict = combine(*blocks_w_jdict) + + blocks_w_ndict = [rbc.household, J_firm.nesteddict, rbc.mkt_clearing] + cblock_w_ndict = combine(*blocks_w_ndict) + + # Ensure that the JacobianDict and the raw nested dict were properly converted to JacobianDictBlocks after + # the use of combine + assert isinstance(cblock_w_jdict._blocks_unsorted[1], JacobianDictBlock) + assert isinstance(cblock_w_ndict._blocks_unsorted[1], JacobianDictBlock) From c32887f1b1ede9f962c9a285aa2f6c4ee64f2ae7 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 31 Mar 2021 11:17:59 -0500 Subject: [PATCH 089/288] Change helper_indices convention to helper_blocks convention (see Github Issue #9) and change combine(*blocks) -> combine(blocks) for consistency of blocks + helper_blocks syntax --- sequence_jacobian/blocks/combined_block.py | 63 +++++++----------- sequence_jacobian/blocks/solved_block.py | 8 +-- sequence_jacobian/jacobian/drivers.py | 4 +- sequence_jacobian/nonlinear.py | 4 +- sequence_jacobian/primitives.py | 4 +- sequence_jacobian/steady_state/drivers.py | 38 +++++------ sequence_jacobian/utilities/graph.py | 74 ++++++++-------------- sequence_jacobian/utilities/misc.py | 4 -- tests/base/test_jacobian.py | 2 +- tests/base/test_jacobian_dict_block.py | 4 +- tests/base/test_transitional_dynamics.py | 5 +- tests/conftest.py | 33 ++++++---- tests/robustness/test_steady_state.py | 13 +++- 13 files changed, 113 insertions(+), 143 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index cc4ccfe..6b02d0a 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -11,14 +11,13 @@ from ..jacobian.classes import JacobianDict -def combine(*args, name="", helper_indices=None, model_alias=False): - # TODO: Implement a check that all args are child types of AbstractBlock, when that is properly implemented - return CombinedBlock(*args, name=name, helper_indices=helper_indices, model_alias=model_alias) +def combine(blocks, name="", model_alias=False): + return CombinedBlock(blocks, name=name, model_alias=model_alias) # Useful functional alias -def create_model(*args, **kwargs): - return combine(*args, model_alias=True, **kwargs) +def create_model(blocks, **kwargs): + return combine(blocks, model_alias=True, **kwargs) class CombinedBlock(Block): @@ -28,24 +27,12 @@ class CombinedBlock(Block): # To users: Do *not* manually change the attributes via assignment. Instantiating a # CombinedBlock has some automated features that are inferred from initial instantiation but not from # re-assignment of attributes post-instantiation. - def __init__(self, *blocks, name="", helper_indices=None, model_alias=False): + def __init__(self, blocks, name="", model_alias=False): - # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] - - # Upon instantiation, we only have enough information to conduct a sort ignoring helper blocks - # since we need a `calibration` to resolve cyclic dependencies when including helper blocks in a topological sort - # Hence, we will cache that info upon first invocation of the steady_state - self.helper_indices = helper_indices if helper_indices is not None else [] - self._sorted_indices_w_o_helpers = utils.graph.block_sort([b for i, b in enumerate(blocks) if i not in self.helper_indices]) - self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated - self._required = utils.graph.find_outputs_that_are_intermediate_inputs([b for i, b in enumerate(blocks) if i not in self.helper_indices]) - - # User-facing attributes for accessing blocks - # .blocks_w_helpers meant to only interface with steady_state functionality - # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) - self.blocks_w_helpers = None - self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] + self._sorted_indices = utils.graph.block_sort(blocks) + self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" @@ -68,19 +55,17 @@ def __repr__(self): else: return f"" - def steady_state(self, calibration, **kwargs): + def steady_state(self, calibration, helper_blocks=None, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" - # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for helper blocks - if self._sorted_indices_w_helpers is None: - self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, - helper_indices=self.helper_indices, - calibration=calibration) - self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] + if helper_blocks is None: + helper_blocks = [] + + topsorted = utils.graph.block_sort(self._blocks_unsorted, calibration=calibration, helper_blocks=helper_blocks) + blocks_all = self.blocks + helper_blocks ss_partial_eq = deepcopy(calibration) - for block in self.blocks_w_helpers: - ss_partial_eq.update(eval_block_ss(block, ss_partial_eq, **kwargs)) + for i in topsorted: + ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) return ss_partial_eq def impulse_nonlinear(self, ss, exogenous, **kwargs): @@ -128,22 +113,18 @@ def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_sav return J_partial_eq - def solve_steady_state(self, calibration, unknowns, targets, solver=None, **kwargs): + def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, + sort_blocks=False, **kwargs): """Evaluate a general equilibrium steady state of the CombinedBlock given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for helper blocks - if self._sorted_indices_w_helpers is None: - self._sorted_indices_w_helpers = utils.graph.block_sort(self._blocks_unsorted, ignore_helpers=False, - helper_indices=self.helper_indices, - calibration=calibration) - self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] - if solver is None: solver = provide_solver_default(unknowns) + if helper_blocks and sort_blocks is False: + sort_blocks = True - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, sort_blocks=False, **kwargs) + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, + helper_blocks=helper_blocks, sort_blocks=sort_blocks, **kwargs) # Useful type aliases diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 3546a60..e892160 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -43,9 +43,9 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort # Hence, we will cache that info upon first invocation of the steady_state - self._sorted_indices_w_o_helpers = graph.block_sort(blocks, ignore_helpers=True) + self._sorted_indices_w_o_helpers = graph.block_sort(blocks) self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated - self._required = graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + self._required = graph.find_outputs_that_are_intermediate_inputs(blocks) # User-facing attributes for accessing blocks # .blocks_w_helpers meant to only interface with steady_state functionality @@ -84,11 +84,11 @@ def jac(self, ss, T=None, shock_list=None, **kwargs): DeprecationWarning) return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, calibration, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False): + def steady_state(self, calibration, helper_blocks=None, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: - self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, ignore_helpers=False, + self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, helper_blocks=helper_blocks, calibration=calibration) self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 08e9cc1..a9dc512 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -194,8 +194,8 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, save=False, use_saved=False): """ # step 1: get topological sort and required - topsorted = graph.block_sort(blocks, ignore_helpers=True) - required = graph.find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=True) + topsorted = graph.block_sort(blocks) + required = graph.find_outputs_that_are_intermediate_inputs(blocks) # Remove any vector-valued outputs that are intermediate inputs, since we don't want # to compute Jacobians with respect to vector-valued variables diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 9197d1f..58edf0c 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -59,7 +59,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factore H_U_factored = misc.factor(H_U) # do a topological sort once to avoid some redundancy - sort = graph.block_sort(block_list, ignore_helpers=True) + sort = graph.block_sort(block_list) # iterate until convergence for it in range(maxit): @@ -98,7 +98,7 @@ def td_map(block_list, ss, exogenous, unknowns=None, sort=None, # first get topological sort if none already provided if sort is None: - sort = graph.block_sort(block_list, ignore_helpers=True) + sort = graph.block_sort(block_list) # initialize results results = {**exogenous, **unknowns} diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 2fd56f8..6163c64 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -92,7 +92,9 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks_w_helpers if hasattr(self, "blocks_w_helpers") else [self] + blocks = self.blocks if hasattr(self, "blocks") else [self] + if "helper_blocks" in kwargs and kwargs["helper_blocks"] is not None: + blocks = blocks + kwargs["helper_blocks"] solver = solver if solver else provide_solver_default(unknowns) return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 884df5a..4ea2e6d 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -10,7 +10,7 @@ # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, helper_indices=None, sort_blocks=True, +def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sort_blocks=True, consistency_check=True, ttol=2e-12, ctol=1e-9, block_kwargs=None, verbose=False, fragile=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): @@ -25,9 +25,8 @@ def steady_state(blocks, calibration, unknowns, targets, helper_indices=None, so A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver targets: `dict` A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG - helper_indices: `list` - A list of indices (int) indicating which blocks in `blocks` are "helper blocks", i.e. SimpleBlocks that replace - some of the equations in the DAG for the purposes of aiding steady state calculation + helper_blocks: `list` + A list of blocks that replace some of the equations in the DAG to aid steady state calculation sort_blocks: `bool` Whether the blocks need to be topologically sorted (only False when this function is called from within a Block object, like CombinedBlock, that has already pre-sorted the blocks) @@ -64,8 +63,8 @@ def steady_state(blocks, calibration, unknowns, targets, helper_indices=None, so """ # Populate otherwise mutable default arguments - if helper_indices is None: - helper_indices = [] + if helper_blocks is None: + helper_blocks = [] if block_kwargs is None: block_kwargs = {} if solver_kwargs is None: @@ -73,14 +72,15 @@ def steady_state(blocks, calibration, unknowns, targets, helper_indices=None, so if constrained_kwargs is None: constrained_kwargs = {} + blocks_all = blocks + helper_blocks + ss_values = deepcopy(calibration) if sort_blocks: - topsorted = graph.block_sort(blocks, ignore_helpers=False, - helper_indices=helper_indices, calibration=calibration) + topsorted = graph.block_sort(blocks, calibration=calibration, helper_blocks=helper_blocks) else: - topsorted = range(len(blocks)) + topsorted = range(len(blocks + helper_blocks)) - def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False): + def residual(unknown_values, include_helpers=True, update_unknowns_in_place=False): ss_values.update(misc.smart_zip(unknowns.keys(), unknown_values)) helper_outputs = {} @@ -88,16 +88,16 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Progress through the DAG computing the resulting steady state values based on the unknown_values # provided to the residual function for i in topsorted: - if not include_helpers and i in helper_indices: + if not include_helpers and blocks_all[i] in helper_blocks: continue # Want to see hetoutputs - elif hasattr(blocks[i], 'hetoutput') and blocks[i].hetoutput is not None: - outputs = eval_block_ss(blocks[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) + elif hasattr(blocks_all[i], 'hetoutput') and blocks_all[i].hetoutput is not None: + outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) ss_values.update(misc.dict_diff(outputs, helper_outputs)) else: - outputs = eval_block_ss(blocks[i], ss_values, consistency_check=consistency_check, + outputs = eval_block_ss(blocks_all[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) - if include_helpers and i in helper_indices: + if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update(outputs) ss_values.update(outputs) else: @@ -109,7 +109,7 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # i.e. the "unknowns" in the namespace in which this function is invoked will change! # Useful for a) if the unknown values are updated while iterating each blocks' ss computation within the DAG, # and/or b) if the user wants to update "unknowns" in place for use in other computations. - if update_unknowns_inplace: + if update_unknowns_in_place: unknowns.update(misc.smart_zip(unknowns.keys(), [ss_values[key] for key in unknowns.keys()])) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the @@ -128,10 +128,6 @@ def residual(unknown_values, include_helpers=True, update_unknowns_inplace=False # Update to set the solutions for the steady state values of the unknowns ss_values.update(zip(unknowns, misc.make_tuple(unknown_solutions))) - # Find the hetoutputs of the Hetblocks that have hetoutputs - for i in misc.find_blocks_with_hetoutputs(blocks): - ss_values.update(eval_block_ss(blocks[i], ss_values, hetoutput=True, **block_kwargs)) - return ss_values @@ -224,7 +220,7 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, # Call residual() once to update ss_values and to check the targets match the provided solution. # The initial value passed into residual (np.zeros()) in this case is irrelevant, but something # must still be passed in since the residual function requires an argument. - assert abs(np.max(residual(misc.smart_zeros(len(unknowns)), update_unknowns_inplace=True))) < tol + assert abs(np.max(residual(misc.smart_zeros(len(unknowns)), update_unknowns_in_place=True))) < tol unknown_solutions = list(unknowns.values()) else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") diff --git a/sequence_jacobian/utilities/graph.py b/sequence_jacobian/utilities/graph.py index bc541d4..aa01ecd 100644 --- a/sequence_jacobian/utilities/graph.py +++ b/sequence_jacobian/utilities/graph.py @@ -1,7 +1,7 @@ """Topological sort and related code""" -def block_sort(blocks, ignore_helpers=False, helper_indices=None, calibration=None, return_io=False): +def block_sort(blocks, helper_blocks=None, calibration=None, return_io=False): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's @@ -32,38 +32,27 @@ def block_sort(blocks, ignore_helpers=False, helper_indices=None, calibration=No return_io: `bool` A boolean indicating whether to return the full set of input and output arguments from `blocks` """ - if helper_indices is None: - helper_indices = [] - # TODO: Decide whether we want to break out the input and output argument tracking and return to # a different function... currently it's very convenient to slot it into block_sort directly, but it # does clutter up the function body if return_io: # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(blocks, ignore_helpers=ignore_helpers, - helper_indices=helper_indices, return_output_args=True) + outmap, outargs = construct_output_map(blocks, helper_blocks=helper_blocks, + return_output_args=True) # step 2: dependency graph for topological sort and input list dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, - ignore_helpers=ignore_helpers, - helper_indices=helper_indices, calibration=calibration) + helper_blocks=helper_blocks, calibration=calibration) - if ignore_helpers: - return [i for i in topological_sort(dep) if i not in helper_indices], inargs, outargs - else: - return topological_sort(dep), inargs, outargs + return topological_sort(dep), inargs, outargs else: # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(blocks, ignore_helpers=ignore_helpers, helper_indices=helper_indices) + outmap = construct_output_map(blocks, helper_blocks=helper_blocks) # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(blocks, outmap, calibration=calibration, - ignore_helpers=ignore_helpers, helper_indices=helper_indices) + dep = construct_dependency_graph(blocks, outmap, calibration=calibration, helper_blocks=helper_blocks) - if ignore_helpers: - return [i for i in topological_sort(dep) if i not in helper_indices] - else: - return topological_sort(dep) + return topological_sort(dep) def topological_sort(dep, names=None): @@ -93,27 +82,23 @@ def topological_sort(dep, names=None): return topsorted -def construct_output_map(blocks, ignore_helpers=False, helper_indices=None, return_output_args=False): +def construct_output_map(blocks, helper_blocks=None, return_output_args=False): """Construct a map of outputs to the indices of the blocks that produce them. blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, helper block, etc.) to sort - ignore_helpers: `bool` - A boolean indicating whether to account for/return the indices of helper blocks contained in blocks - Set to true when sorting for td and jac calculations + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + helper_blocks: `list` + A list of helper blocks, designed to aid steady state computation, to include in the sort return_output_args: `bool` A boolean indicating whether to track and return the full set of output arguments of all of the blocks in `blocks` """ - if helper_indices is None: - helper_indices = [] + if helper_blocks is None: + helper_blocks = [] outmap = dict() outargs = set() - for num, block in enumerate(blocks): - if ignore_helpers and num in helper_indices: - continue - + for num, block in enumerate(blocks + helper_blocks): # Find the relevant set of outputs corresponding to a block if hasattr(block, "outputs"): outputs = block.outputs @@ -126,12 +111,13 @@ def construct_output_map(blocks, ignore_helpers=False, helper_indices=None, retu # Because some of the outputs of a helper block are, by construction, outputs that also appear in the # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering # throwing this ValueError - if o in outmap and not (num in helper_indices or outmap[o] in helper_indices): + if o in outmap and block not in helper_blocks: raise ValueError(f'{o} is output twice') + # Priority sorting for standard blocks: # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap or (o in outmap and num not in helper_indices): + if o not in outmap: outmap[o] = num if return_output_args: outargs.add(o) @@ -143,7 +129,7 @@ def construct_output_map(blocks, ignore_helpers=False, helper_indices=None, retu return outmap -def construct_dependency_graph(blocks, outmap, ignore_helpers=False, helper_indices=None, +def construct_dependency_graph(blocks, outmap, helper_blocks=None, calibration=None, return_input_args=False): """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where this set is the set of blocks that the key block is dependent on. @@ -154,14 +140,12 @@ def construct_dependency_graph(blocks, outmap, ignore_helpers=False, helper_indi """ if calibration is None: calibration = {} - if helper_indices is None: - helper_indices = [] + if helper_blocks is None: + helper_blocks = [] - dep = {num: set() for num in range(len(blocks))} + dep = {num: set() for num in range(len(blocks + helper_blocks))} inargs = set() - for num, block in enumerate(blocks): - if ignore_helpers and num in helper_indices: - continue + for num, block in enumerate(blocks + helper_blocks): if hasattr(block, 'inputs'): inputs = block.inputs else: @@ -177,7 +161,7 @@ def construct_dependency_graph(blocks, outmap, ignore_helpers=False, helper_indi # produces that input as an output as a dependency. # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and num in helper_indices): + if i in outmap and not (i in calibration and block in helper_blocks): dep[num].add(outmap[i]) if return_input_args: return dep, inargs @@ -185,20 +169,18 @@ def construct_dependency_graph(blocks, outmap, ignore_helpers=False, helper_indi return dep -def find_outputs_that_are_intermediate_inputs(blocks, ignore_helpers=False, helper_indices=None): +def find_outputs_that_are_intermediate_inputs(blocks, helper_blocks=None): """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. See the docstring of construct_output_map for more details about the arguments. """ - if helper_indices is None: - helper_indices = [] + if helper_blocks is None: + helper_blocks = [] required = set() - outmap = construct_output_map(blocks, ignore_helpers=ignore_helpers, helper_indices=helper_indices) + outmap = construct_output_map(blocks, helper_blocks=helper_blocks) for num, block in enumerate(blocks): - if ignore_helpers and num in helper_indices: - continue if hasattr(block, 'inputs'): inputs = block.inputs else: diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 2833ac6..b9ba657 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -145,7 +145,3 @@ def smart_zeros(n): return np.zeros(n) else: return 0. - - -def find_blocks_with_hetoutputs(blocks): - return [i for i, block in enumerate(blocks) if hasattr(block, "hetoutput") and block.hetoutput is not None] diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 7404d46..55e795e 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -8,7 +8,7 @@ def test_ks_jac(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag - household, firm, mkt_clearing, _, _, _ = ks_model._blocks_unsorted + household, firm, mkt_clearing, _, _ = ks_model._blocks_unsorted T = 10 # Automatically calculate the general equilibrium Jacobian diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 992443c..041bf59 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -32,10 +32,10 @@ def test_jacobian_dict_block_combine(rbc_dag): J_firm = rbc.firm.jacobian(ss, exogenous=exogenous) blocks_w_jdict = [rbc.household, J_firm, rbc.mkt_clearing] - cblock_w_jdict = combine(*blocks_w_jdict) + cblock_w_jdict = combine(blocks_w_jdict) blocks_w_ndict = [rbc.household, J_firm.nesteddict, rbc.mkt_clearing] - cblock_w_ndict = combine(*blocks_w_ndict) + cblock_w_ndict = combine(blocks_w_ndict) # Ensure that the JacobianDict and the raw nested dict were properly converted to JacobianDictBlocks after # the use of combine diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index a011017..4d04517 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -93,9 +93,8 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): blocks_simple = [household, two_asset.make_grids, two_asset.pricing, two_asset.arbitrage, two_asset.labor, two_asset.investment, two_asset.dividend, two_asset.taylor, two_asset.fiscal, - two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing, - two_asset.partial_steady_state_solution] - two_asset_model_simple = combine(*blocks_simple, name="Two-Asset HANK w/ SimpleBlocks", helper_indices=[13]) + two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] + two_asset_model_simple = combine(blocks_simple, name="Two-Asset HANK w/ SimpleBlocks") unknowns_simple = ["r", "w", "Y", "pi", "p", "Q", "K"] targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] diff --git a/tests/conftest.py b/tests/conftest.py index b8b1575..4af9595 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,14 +9,16 @@ @pytest.fixture(scope='session') def rbc_dag(): - blocks = [rbc.household, rbc.mkt_clearing, rbc.firm, rbc.steady_state_solution] - rbc_model = create_model(*blocks, name="RBC", helper_indices=[3]) + blocks = [rbc.household, rbc.mkt_clearing, rbc.firm] + helper_blocks = [rbc.steady_state_solution] + rbc_model = create_model(blocks, name="RBC") # Steady State calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "frisch": 1., "L": 1.0, "r": 0.01} unknowns_ss = {"beta": None, "vphi": None} targets_ss = {"goods_mkt": 0, "euler": 0} - ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="solved") + ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + helper_blocks=helper_blocks, solver="solved") # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] @@ -29,15 +31,17 @@ def rbc_dag(): @pytest.fixture(scope='session') def krusell_smith_dag(): blocks = [krusell_smith.household, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars, - krusell_smith.asset_state_vars, krusell_smith.firm_steady_state_solution] - ks_model = create_model(*blocks, name="Krusell-Smith", helper_indices=[5]) + krusell_smith.asset_state_vars] + helper_blocks = [krusell_smith.firm_steady_state_solution] + ks_model = create_model(blocks, name="Krusell-Smith") # Steady State calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01} unknowns_ss = {"beta": (0.98/1.01, 0.999/1.01)} targets_ss = {"K": "A"} - ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq") + ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + helper_blocks=helper_blocks, solver="brentq") # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] @@ -50,8 +54,9 @@ def krusell_smith_dag(): @pytest.fixture(scope='session') def one_asset_hank_dag(): blocks = [hank.household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc, - hank.income_state_vars, hank.asset_state_vars, hank.partial_steady_state_solution] - hank_model = create_model(*blocks, name="One-Asset HANK", helper_indices=[8]) + hank.income_state_vars, hank.asset_state_vars] + helper_blocks = [hank.partial_steady_state_solution] + hank_model = create_model(blocks, name="One-Asset HANK") # Steady State calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, @@ -59,7 +64,8 @@ def one_asset_hank_dag(): "pi": 0, "nS": 2, "amax": 150, "nA": 10} unknowns_ss = {"beta": 0.986, "vphi": 0.8} targets_ss = {"asset_mkt": 0, "labor_mkt": 0} - ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom") + ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + helper_blocks=helper_blocks, solver="broyden_custom") # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z"] @@ -76,9 +82,9 @@ def two_asset_hank_dag(): blocks = [household, two_asset.make_grids, two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved, two_asset.dividend, two_asset.taylor, two_asset.fiscal, - two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing, - two_asset.partial_steady_state_solution] - two_asset_model = create_model(*blocks, name="Two-Asset HANK", helper_indices=[12]) + two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] + helper_blocks = [two_asset.partial_steady_state_solution] + two_asset_model = create_model(blocks, name="Two-Asset HANK") # Steady State calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, @@ -88,7 +94,8 @@ def two_asset_hank_dag(): "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns_ss = {"beta": 0.976, "vphi": 2.07, "chi1": 6.5} targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} - ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom") + ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + helper_blocks=helper_blocks, solver="broyden_custom") # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z", "G"] diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index c28225d..312aaac 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -1,18 +1,23 @@ +"""Tests for steady_state with worse initial guesses, making use of the constrained solution functionality""" + import pytest import numpy as np +from sequence_jacobian.models import hank, two_asset # Filter out warnings when the solver is trying to search in bad regions @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): hank_model, _, _, _, ss = one_asset_hank_dag + helper_blocks = [hank.partial_steady_state_solution] + calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} unknowns = {"beta": (0.95, 0.97, 0.999/(1 + 0.005)), "vphi": (0.001, 1.0, 10)} targets = {"asset_mkt": 0, "labor_mkt": 0} - ss_ref = hank_model.solve_steady_state(calibration, unknowns, targets, + ss_ref = hank_model.solve_steady_state(calibration, unknowns, targets, helper_blocks=helper_blocks, solver="broyden1", solver_kwargs={"options": {"maxiter": 250}}, constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) @@ -24,6 +29,8 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag): two_asset_model, _, _, _, ss = two_asset_hank_dag + helper_blocks = [two_asset.partial_steady_state_solution] + calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, "tot_wealth": 14, "K": 10, "delta": 0.02, "kappap": 0.1, "muw": 1.1, "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, @@ -31,7 +38,7 @@ def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns = {"beta": (0.5, 0.9, 0.999 / (1 + 0.0125)), "vphi": (0.001, 1.0, 10.), "chi1": (0.5, 5.5, 10.)} targets = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} - ss_ref = two_asset_model.solve_steady_state(calibration, unknowns, targets, solver="broyden_custom", - consistency_check=True) + ss_ref = two_asset_model.solve_steady_state(calibration, unknowns, targets, helper_blocks=helper_blocks, + solver="broyden_custom", consistency_check=True) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) From ea2e5449d4d0068b75f09f9f0ee5213805caf504 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 1 Apr 2021 17:02:02 -0500 Subject: [PATCH 090/288] Implement list_diff (works analogously to dict_diff) --- sequence_jacobian/utilities/misc.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index b9ba657..05ce708 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -120,14 +120,21 @@ def unprime(s): return s +def list_diff(l1, l2): + """Returns the list that is the "set difference" between l1 and l2 (based on element values)""" + o_dict = {} + for ik, k in enumerate(set(l1) - set(l2)): + o_dict[k] = l1[ik] + return o_dict + + def dict_diff(d1, d2): """Returns the dictionary that is the "set difference" between d1 and d2 (based on keys, not key-value pairs) E.g. d1 = {"a": 1, "b": 2}, d2 = {"b": 5}, then dict_diff(d1, d2) = {"a": 1} """ o_dict = {} - for k in set(d1.keys()).difference(set(d2.keys())): + for k in set(d1.keys()) - set(d2.keys()): o_dict[k] = d1[k] - return o_dict From 9e9653ce22ac6542e5ac89c7966e5d0ba7d53e84 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 1 Apr 2021 17:04:10 -0500 Subject: [PATCH 091/288] Implement helper block subsetting from the main steady state solution loop --- sequence_jacobian/steady_state/drivers.py | 93 +++++++++++++++++------ sequence_jacobian/steady_state/support.py | 19 +++++ 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 4ea2e6d..755fc53 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -3,16 +3,18 @@ import numpy as np import scipy.optimize as opt from copy import deepcopy +from functools import partial from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ - extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check + extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ + subset_helper_block_unknowns_and_targets from ..utilities import solvers, graph, misc # Find the steady state solution def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sort_blocks=True, - consistency_check=True, ttol=2e-12, ctol=1e-9, - block_kwargs=None, verbose=False, fragile=False, solver=None, solver_kwargs=None, + consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, + block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. @@ -38,14 +40,14 @@ def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sor ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the use of helper blocks, to equal the desired values + fragile: `bool` + Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails block_kwargs: `dict` A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that Block sub-class. verbose: `bool` Display the content of optional print statements within the solver for more responsive feedback - fragile: `bool` - Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails solver: `string` The name of the numerical solver that the user would like to user. Can either be a custom solver the user implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root @@ -72,19 +74,28 @@ def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sor if constrained_kwargs is None: constrained_kwargs = {} + # Initial setup of blocks, targets, and dictionary of steady state values to be returned blocks_all = blocks + helper_blocks + targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + + helper_unknowns, helper_targets = subset_helper_block_unknowns_and_targets(helper_blocks, unknowns, targets) ss_values = deepcopy(calibration) + ss_values.update(helper_targets) + if sort_blocks: - topsorted = graph.block_sort(blocks, calibration=calibration, helper_blocks=helper_blocks) + topsorted = graph.block_sort(blocks, calibration=helper_targets, helper_blocks=helper_blocks) else: topsorted = range(len(blocks + helper_blocks)) - def residual(unknown_values, include_helpers=True, update_unknowns_in_place=False): - ss_values.update(misc.smart_zip(unknowns.keys(), unknown_values)) + def residual(targets_dict, unknown_keys, *unknown_values, + include_helpers=True, update_unknowns_in_place=False): + ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) helper_outputs = {} + # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper + # block subsetting # Progress through the DAG computing the resulting steady state values based on the unknown_values # provided to the residual function for i in topsorted: @@ -110,23 +121,54 @@ def residual(unknown_values, include_helpers=True, update_unknowns_in_place=Fals # Useful for a) if the unknown values are updated while iterating each blocks' ss computation within the DAG, # and/or b) if the user wants to update "unknowns" in place for use in other computations. if update_unknowns_in_place: - unknowns.update(misc.smart_zip(unknowns.keys(), [ss_values[key] for key in unknowns.keys()])) + unknowns.update(misc.smart_zip(unknown_keys, [ss_values[key] for key in unknown_keys])) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" - return compute_target_values(targets, ss_values) + return compute_target_values(targets_dict, ss_values) - unknown_solutions = _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, - constrained_method, constrained_kwargs, - tol=ttol, verbose=verbose) + if helper_blocks: + # Initial verification that helper block targets are satisfied by the helper blocks + unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1])/2 for v in unknowns.values()] + targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), *unknowns_init_vals))) + + # Subset out the unknowns and targets that are not handled by helper blocks + unknowns_w_o_helpers = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), helper_unknowns)} + targets_w_o_helpers = misc.dict_diff(targets, helper_targets) + + # Assumption: If the targets handled by helpers are satisfied under the provided + # set of unknown variables' initial values, then it is assumed they will be under other unknown variables' + # initial values and hence the unknowns/targets handled by helpers and the helper blocks themselves + # can be omitted from the DAG when solving. + if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): + unknown_solutions = _solve_for_unknowns(partial(residual, targets_w_o_helpers, unknowns_w_o_helpers.keys(), + include_helpers=False), + unknowns_w_o_helpers, solver, solver_kwargs, + constrained_method, constrained_kwargs, + tol=ttol, verbose=verbose) + # If targets handled by helpers are not satisfied then it is assumed that helper blocks merely aid in providing + # more accurate guesses for the DAG solution and they remain a part of the DAG when solving. + else: + unknown_solutions = _solve_for_unknowns(partial(residual, targets, unknowns.keys()), + unknowns, solver, solver_kwargs, + constrained_method, constrained_kwargs, + tol=ttol, verbose=verbose) + else: + unknown_solutions = _solve_for_unknowns(partial(residual, targets, unknowns.keys()), + unknowns, solver, solver_kwargs, + constrained_method, constrained_kwargs, + tol=ttol, verbose=verbose) # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check: - cresid = abs(np.max(residual(unknown_solutions, include_helpers=False))) + unknown_solutions = misc.make_tuple(unknown_solutions) + unknowns_solved = {k: unknown_solutions[ik] for ik, k in enumerate(unknowns_w_o_helpers)} + unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) + cresid = abs(np.max(residual(targets, unknowns_solved.keys(), *unknowns_solved.values(), include_helpers=False))) run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns - ss_values.update(zip(unknowns, misc.make_tuple(unknown_solutions))) + ss_values.update(misc.smart_zip(unknowns, unknown_solutions)) return ss_values @@ -152,6 +194,8 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, and returns computed targets. unknowns: `dict` Refer to the `steady_state` function docstring for the "unknowns" variable + targets: `dict` + Refer to the `steady_state` function docstring for the "targets" variable tol: `float` The absolute convergence tolerance of the computed target to the desired target value in the numerical solver solver: `str` @@ -169,7 +213,8 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") elif solver in scipy_optimize_uni_solvers: initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) - result = opt.root_scalar(residual, method=solver, xtol=tol, **initial_values_or_bounds, **solver_kwargs) + result = opt.root_scalar(residual, method=solver, xtol=tol, + **initial_values_or_bounds, **solver_kwargs) if not result.converged: raise ValueError(f"Steady-state solver, {solver}, did not converge.") unknown_solutions = result.root @@ -177,12 +222,14 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - result = opt.root(residual, initial_values, method=solver, tol=tol, **solver_kwargs) + result = opt.root(residual, initial_values, + method=solver, tol=tol, **solver_kwargs) else: constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) - result = opt.root(constrained_residual, initial_values, method=solver, tol=tol, **solver_kwargs) + result = opt.root(constrained_residual, initial_values, + method=solver, tol=tol, **solver_kwargs) if not result.success: raise ValueError(f"Steady-state solver, {solver}, did not converge." f" The termination status is {result.status}.") @@ -193,8 +240,8 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = solvers.broyden_solver(residual, initial_values, tol=tol, - verbose=verbose, **solver_kwargs) + unknown_solutions, _ = solvers.broyden_solver(residual, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) else: constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, method=constrained_method, @@ -206,14 +253,14 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = solvers.newton_solver(residual, initial_values, tol=tol, - verbose=verbose, **solver_kwargs) + unknown_solutions, _ = solvers.newton_solver(residual, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) else: constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, - verbose=verbose, tol=tol, **solver_kwargs) + tol=tol, verbose=verbose, **solver_kwargs) unknown_solutions = list(unknown_solutions) elif solver == "solved": # If the entire solution is provided by the helper blocks diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index 940dff2..e011c36 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -71,6 +71,25 @@ def compute_target_values(targets, potential_args): return target_values +def subset_helper_block_unknowns_and_targets(helper_blocks, unknowns, targets): + """Find the set of unknowns and targets that the `helper_blocks` solve out""" + unknowns_handled_by_helpers = set() + targets_handled_by_helpers = set() + for block in helper_blocks: + unknowns_handled_by_helpers |= (block.inputs | block.outputs) & set(unknowns.keys()) + targets_handled_by_helpers |= (block.inputs | block.outputs) & set(targets.keys()) + unknowns_handled_by_helpers = list(unknowns_handled_by_helpers) + targets_handled_by_helpers = list(targets_handled_by_helpers) + + n_unknowns = len(unknowns_handled_by_helpers) + n_targets = len(targets_handled_by_helpers) + if n_unknowns != n_targets: + raise ValueError(f"The provided helper_blocks handle {n_unknowns} unknowns != {n_targets} targets." + f" User must specify an equal number of unknowns/targets solved for by helper blocks.") + + return unknowns_handled_by_helpers, dict(zip(targets_handled_by_helpers, [targets[t] for t in targets_handled_by_helpers])) + + def extract_univariate_initial_values_or_bounds(unknowns): val = next(iter(unknowns.values())) if np.isscalar(val): From 028170454c506be2ece0093a93ff6449dfd8095a Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 12:06:43 -0500 Subject: [PATCH 092/288] Fix wrong implementation of list_diff --- sequence_jacobian/utilities/misc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 05ce708..09cef95 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -122,10 +122,10 @@ def unprime(s): def list_diff(l1, l2): """Returns the list that is the "set difference" between l1 and l2 (based on element values)""" - o_dict = {} - for ik, k in enumerate(set(l1) - set(l2)): - o_dict[k] = l1[ik] - return o_dict + o_list = [] + for k in set(l1) - set(l2): + o_list.append(k) + return o_list def dict_diff(d1, d2): From d63bdaee0605f01d84aaf3ab4ea59f09e21b8097 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 12:08:52 -0500 Subject: [PATCH 093/288] Allow two-argument unknown initial values (lower and upper bounds) for multivariate solver, notes below. This was implemented specifically to allow symmetry of calling solve_steady_state() with and without helper blocks, where in the case of Krusell Smith, we need to use a multivariate solver when not using helper blocks, but we need to use a univariate solver when using helper blocks. The multivariate solver requires initial values, whereas the univariate solver requires bounds. To impose a uniform interface, we take the midpoint as the initial value for the multivariate solver when bounds are provided. --- sequence_jacobian/steady_state/support.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index e011c36..4b51609 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -98,7 +98,7 @@ def extract_univariate_initial_values_or_bounds(unknowns): return {"bracket": (val[0], val[1])} -def extract_multivariate_initial_values_and_bounds(unknowns): +def extract_multivariate_initial_values_and_bounds(unknowns, fragile=False): """Provided a dict mapping names of unknowns to initial values/bounds, return separate dicts of the initial values and bounds. Note: For one-sided bounds, simply put np.inf/-np.inf as the other side of the bounds, so there is @@ -109,6 +109,17 @@ def extract_multivariate_initial_values_and_bounds(unknowns): for k, v in unknowns.items(): if np.isscalar(v): initial_values.append(v) + elif len(v) == 2: + if fragile: + raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." + f" the values of `unknowns` must either be a scalar, pertaining to a" + f" single initial value for the root solver to begin from," + f" a length 2 tuple, pertaining to a lower bound and an upper bound," + f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") + else: + warnings.warn("Interpreting values of `unknowns` from length 2 tuple as lower and upper bounds" + " and averaging them to get a scalar initial value to provide to the solver.") + initial_values.append((v[0] + v[1])/2) elif len(v) == 3: lb, iv, ub = v assert lb < iv < ub @@ -118,6 +129,7 @@ def extract_multivariate_initial_values_and_bounds(unknowns): raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." f" the values of `unknowns` must either be a scalar, pertaining to a" f" single initial value for the root solver to begin from," + f" a length 2 tuple, pertaining to a lower bound and an upper bound," f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") return np.asarray(initial_values), multi_bounds From ff28a2c68e9676dc80096f4abc2a3fb314e24e8e Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 12:10:41 -0500 Subject: [PATCH 094/288] Enhance the constructed residual function within steady state to only solve for a subset of the total number of unknowns/targets when using helper blocks --- sequence_jacobian/steady_state/drivers.py | 130 ++++++++++++---------- 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 755fc53..6d82e39 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -88,8 +88,7 @@ def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sor else: topsorted = range(len(blocks + helper_blocks)) - def residual(targets_dict, unknown_keys, *unknown_values, - include_helpers=True, update_unknowns_in_place=False): + def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) helper_outputs = {} @@ -116,59 +115,33 @@ def residual(targets_dict, unknown_keys, *unknown_values, # been solved for in helper_blocks so we can check for consistency after-the-fact ss_values.update(misc.dict_diff(outputs, helper_outputs)) - # Update the "unknowns" dictionary *in place* with its steady state values. - # i.e. the "unknowns" in the namespace in which this function is invoked will change! - # Useful for a) if the unknown values are updated while iterating each blocks' ss computation within the DAG, - # and/or b) if the user wants to update "unknowns" in place for use in other computations. - if update_unknowns_in_place: - unknowns.update(misc.smart_zip(unknown_keys, [ss_values[key] for key in unknown_keys])) - # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" return compute_target_values(targets_dict, ss_values) if helper_blocks: - # Initial verification that helper block targets are satisfied by the helper blocks - unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1])/2 for v in unknowns.values()] - targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), *unknowns_init_vals))) - - # Subset out the unknowns and targets that are not handled by helper blocks - unknowns_w_o_helpers = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), helper_unknowns)} - targets_w_o_helpers = misc.dict_diff(targets, helper_targets) - - # Assumption: If the targets handled by helpers are satisfied under the provided - # set of unknown variables' initial values, then it is assumed they will be under other unknown variables' - # initial values and hence the unknowns/targets handled by helpers and the helper blocks themselves - # can be omitted from the DAG when solving. - if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): - unknown_solutions = _solve_for_unknowns(partial(residual, targets_w_o_helpers, unknowns_w_o_helpers.keys(), - include_helpers=False), - unknowns_w_o_helpers, solver, solver_kwargs, - constrained_method, constrained_kwargs, - tol=ttol, verbose=verbose) - # If targets handled by helpers are not satisfied then it is assumed that helper blocks merely aid in providing - # more accurate guesses for the DAG solution and they remain a part of the DAG when solving. - else: - unknown_solutions = _solve_for_unknowns(partial(residual, targets, unknowns.keys()), - unknowns, solver, solver_kwargs, - constrained_method, constrained_kwargs, - tol=ttol, verbose=verbose) + unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, + helper_targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=ttol, verbose=verbose, fragile=fragile) else: - unknown_solutions = _solve_for_unknowns(partial(residual, targets, unknowns.keys()), - unknowns, solver, solver_kwargs, - constrained_method, constrained_kwargs, - tol=ttol, verbose=verbose) + unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=ttol, verbose=verbose, fragile=fragile) # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check: - unknown_solutions = misc.make_tuple(unknown_solutions) - unknowns_solved = {k: unknown_solutions[ik] for ik, k in enumerate(unknowns_w_o_helpers)} + # Add the unknowns not handled by helpers into the DAG to be checked. unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) - cresid = abs(np.max(residual(targets, unknowns_solved.keys(), *unknowns_solved.values(), include_helpers=False))) + + cresid = abs(np.max(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), + include_helpers=False))) run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns - ss_values.update(misc.smart_zip(unknowns, unknown_solutions)) + ss_values.update(unknowns_solved) return ss_values @@ -179,9 +152,9 @@ def eval_block_ss(block, calibration, **kwargs): **{k: v for k, v in kwargs.items() if k in misc.input_kwarg_list(block.steady_state)}) -def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, - constrained_method, constrained_kwargs, - tol=2e-12, verbose=False): +def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, + constrained_method="linear_continuation", constrained_kwargs=None, + tol=2e-12, verbose=False, fragile=False): """ Given a residual function (constructed within steady_state) and a set of bounds or initial values for the set of unknowns, solve for the root. @@ -205,27 +178,35 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, return: The root[s] of the residual function as either a scalar (float) or a list of floats """ + if residual_kwargs is None: + residual_kwargs = {} + scipy_optimize_uni_solvers = ["bisect", "brentq", "brenth", "ridder", "toms748", "newton", "secant", "halley"] scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", "excitingmixing", "krylov", "df-sane"] + # Construct a reduced residual function, which contains addl context of unknowns, targets, and keyword arguments. + # This is to bypass issues with passing a residual function that requires contextual, positional arguments + # separate from the unknown values that need to be solved for into the multivariate solvers + residual_f = partial(residual, targets, unknowns.keys(), **residual_kwargs) + if solver is None: raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") elif solver in scipy_optimize_uni_solvers: initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) - result = opt.root_scalar(residual, method=solver, xtol=tol, + result = opt.root_scalar(residual_f, method=solver, xtol=tol, **initial_values_or_bounds, **solver_kwargs) if not result.converged: raise ValueError(f"Steady-state solver, {solver}, did not converge.") unknown_solutions = result.root elif solver in scipy_optimize_multi_solvers: - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns, fragile=fragile) # If no bounds were provided if not bounds: - result = opt.root(residual, initial_values, + result = opt.root(residual_f, initial_values, method=solver, tol=tol, **solver_kwargs) else: - constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) result = opt.root(constrained_residual, initial_values, @@ -240,10 +221,10 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = solvers.broyden_solver(residual, initial_values, + unknown_solutions, _ = solvers.broyden_solver(residual_f, initial_values, tol=tol, verbose=verbose, **solver_kwargs) else: - constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, @@ -253,23 +234,54 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: - unknown_solutions, _ = solvers.newton_solver(residual, initial_values, + unknown_solutions, _ = solvers.newton_solver(residual_f, initial_values, tol=tol, verbose=verbose, **solver_kwargs) else: - constrained_residual = constrained_multivariate_residual(residual, bounds, verbose=verbose, + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, method=constrained_method, **constrained_kwargs) unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, tol=tol, verbose=verbose, **solver_kwargs) unknown_solutions = list(unknown_solutions) elif solver == "solved": - # If the entire solution is provided by the helper blocks - # Call residual() once to update ss_values and to check the targets match the provided solution. - # The initial value passed into residual (np.zeros()) in this case is irrelevant, but something - # must still be passed in since the residual function requires an argument. - assert abs(np.max(residual(misc.smart_zeros(len(unknowns)), update_unknowns_in_place=True))) < tol - unknown_solutions = list(unknowns.values()) + # If the model does not require a numerical solution then return an empty tuple for the unknowns + # that require a numerical solution + unknown_solutions = () else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") + return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) + + +def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, + solver, solver_kwargs, constrained_method="linear_continuation", + constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): + """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets + with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" + # Initial verification that helper block targets are satisfied by the helper blocks + unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] + targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) + + # Subset out the unknowns and targets that are not handled by helper blocks + unknowns_w_o_helpers = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), helper_unknowns)} + targets_w_o_helpers = misc.dict_diff(targets, helper_targets) + + # Assumption: If the targets handled by helpers are satisfied under the provided + # set of unknown variables' initial values then it is assumed they will be under other unknown variables' + # initial values and hence the unknowns/targets handled by helpers and the helper blocks themselves + # can be omitted from the DAG when solving. + if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): + unknown_solutions = _solve_for_unknowns(residual, unknowns_w_o_helpers, targets_w_o_helpers, + solver, solver_kwargs, + residual_kwargs={"include_helpers": False}, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=tol, verbose=verbose, fragile=fragile) + # If targets handled by helpers are not satisfied then it is assumed that helper blocks merely aid in providing + # more accurate guesses for the DAG solution and they remain a part of the DAG when solving. + else: + unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=tol, verbose=verbose, fragile=fragile) return unknown_solutions From 71cc0472357edd3af42e3cf28e3a65868b5800fb Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 12:12:08 -0500 Subject: [PATCH 095/288] (WIP) Update tests and helper blocks within model modules to abide by the new convention of always supplying *all* unknowns/targets (including those handled by helper blocks) at the top level, notes below. Much of this could potentially be incorrect, as I tried to simply infer what are the additional steady state unknowns/targets that need to be included to follow this new convention. This needs to be checked! --- sequence_jacobian/models/hank.py | 6 ++++-- sequence_jacobian/models/rbc.py | 5 ++++- sequence_jacobian/models/two_asset.py | 29 +++++++++++++++++++-------- tests/conftest.py | 16 +++++++-------- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/sequence_jacobian/models/hank.py b/sequence_jacobian/models/hank.py index f181272..02c7724 100644 --- a/sequence_jacobian/models/hank.py +++ b/sequence_jacobian/models/hank.py @@ -161,13 +161,15 @@ def asset_state_vars(amax, nA): @simple -def partial_steady_state_solution(B_Y, mu, r): +def partial_steady_state_solution(B_Y, Y, mu, r, kappa, Z, pi): B = B_Y w = 1 / mu Div = (1 - w) Tax = r * B - return B, w, Div, Tax + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) + + return B, w, Div, Tax, nkpc_res '''Part 3: Steady state''' diff --git a/sequence_jacobian/models/rbc.py b/sequence_jacobian/models/rbc.py index 7ff537b..3268613 100644 --- a/sequence_jacobian/models/rbc.py +++ b/sequence_jacobian/models/rbc.py @@ -40,7 +40,10 @@ def steady_state_solution(r, eis, delta, alpha): beta = 1 / (1 + r) vphi = w * C ** (-1 / eis) - return Z, K, Y, w, I, C, beta, vphi + goods_mkt = Y - C - I + euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) + + return Z, K, Y, w, I, C, beta, vphi, goods_mkt, euler '''Part 2: Steady state''' diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 04d9e3a..dafac75 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -201,7 +201,7 @@ def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): iout[j] = i-1 err_upper = rhs[i, j] - lhs[i] err_lower = rhs[i-1, j] - lhs[i-1] - piout[j] = err_upper / (err_upper - err_lower) + piout[j] = err_upper / (err_upper - err_lower) '''Part 2: Simple blocks''' @@ -279,17 +279,19 @@ def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): @simple def mkt_clearing(p, A, B, Bg, vphi, muw, tax, w, U): - asset_mkt = p + Bg - B - A + wealth = A + B + asset_mkt = p + Bg - wealth labor_mkt = vphi - muw * (1 - tax) * w * U - return asset_mkt, labor_mkt + return asset_mkt, labor_mkt, wealth @simple def mkt_clearing_all(p, A, B, Bg, vphi, muw, tax, w, U, C, I, G, Chi, omega): - asset_mkt = p + Bg - B - A + wealth = A + B + asset_mkt = p + Bg - wealth labor_mkt = vphi - muw * (1 - tax) * w * U goods_mkt = C + I + G + Chi + omega * B - 1 - return asset_mkt, labor_mkt, goods_mkt + return asset_mkt, labor_mkt, goods_mkt, wealth @simple @@ -302,6 +304,12 @@ def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): return b_grid, a_grid, k_grid, e_grid, Pi +@simple +def share_value(p, tot_wealth, Bh): + pshare = p / (tot_wealth - Bh) + return pshare + + @simple def partial_steady_state_solution(delta, K, r, tot_wealth, Bh, Bg, G, omega): I = delta * K @@ -317,7 +325,12 @@ def partial_steady_state_solution(delta, K, r, tot_wealth, Bh, Bg, G, omega): rb = r - omega pshare = p / (tot_wealth - Bh) - return I, mc, alpha, mup, Z, w, tax, div, p, ra, rb, pshare + # TODO: These are completely wrong...but left as is. Change these please! + N = (1. / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) + wnkpc = 0. + wealth = tot_wealth + + return I, mc, alpha, mup, Z, w, tax, div, p, ra, rb, pshare, N, wnkpc, wealth '''Part 3: Steady state''' @@ -404,11 +417,11 @@ def pricing_solved(pi, mc, r, Y, kappap, mup): return nkpc -@solved(unknowns={'p': (10, 15)}, targets=['equity'], solver="brentq") +@solved(unknowns={'p': (5, 15)}, targets=['equity'], solver="brentq") def arbitrage_solved(div, p, r): equity = div(+1) + p(+1) - p * (1 + r(+1)) return equity -production_solved = solved(block_list=[labor, investment], unknowns={'Q': 1, 'K': 10}, +production_solved = solved(block_list=[labor, investment], unknowns={'Q': 1., 'K': 10.}, targets=['inv', 'val'], solver="broyden_custom") diff --git a/tests/conftest.py b/tests/conftest.py index 4af9595..22862dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,9 +14,9 @@ def rbc_dag(): rbc_model = create_model(blocks, name="RBC") # Steady State - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "frisch": 1., "L": 1.0, "r": 0.01} - unknowns_ss = {"beta": None, "vphi": None} - targets_ss = {"goods_mkt": 0, "euler": 0} + calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "frisch": 1., "L": 1.0} + unknowns_ss = {"beta": 0.98, "vphi": 0.95, "Z": 1., "K": 3.} + targets_ss = {"goods_mkt": 0, "euler": 0, "Y": 1., "r": 0.01} ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="solved") @@ -38,8 +38,8 @@ def krusell_smith_dag(): # Steady State calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01} - unknowns_ss = {"beta": (0.98/1.01, 0.999/1.01)} - targets_ss = {"K": "A"} + unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} + targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="brentq") @@ -59,11 +59,11 @@ def one_asset_hank_dag(): hank_model = create_model(blocks, name="One-Asset HANK") # Steady State - calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, + calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B_Y": 5.6, "mu": 1.2, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} - unknowns_ss = {"beta": 0.986, "vphi": 0.8} - targets_ss = {"asset_mkt": 0, "labor_mkt": 0} + unknowns_ss = {"beta": 0.986, "vphi": 0.8, "w": 0.8} + targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="broyden_custom") From d50297f95bf888926ab477c77b58de3e9c17f45b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 29 Mar 2021 14:55:06 -0500 Subject: [PATCH 096/288] Fixed two small mistakes in two-asset HANK: (i) way that muw enters NKWPC, (ii) accounting for the resource cost of capital adjustment. These have negligible effects on results, because vphi scales to offset (i) and (ii) is 2nd order. --- sequence_jacobian/models/two_asset.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index dafac75..ee1ff6a 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -236,9 +236,10 @@ def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): @simple -def dividend(Y, w, N, K, pi, mup, kappap, delta): +def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y - I = K - (1 - delta) * K(-1) + k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) + I = K - (1 - delta) * K(-1) + k_adjust div = Y - w * N - I - psip return psip, I, div @@ -266,13 +267,12 @@ def finance(i, p, pi, r, div, omega, pshare): @simple def wage(pi, w, N, muw, kappaw): piw = (1 + pi) * w / w(-1) - 1 - psiw = muw / (1 - muw) / 2 / kappaw * (1 + piw).apply(np.log) ** 2 * N - return piw, psiw + return piw @simple def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N**(1+1/frisch) - muw*(1-tax)*w*N*U) + beta *\ + wnkpc = kappaw * (vphi * N**(1+1/frisch) - (1-tax)*w*N*U/muw) + beta *\ (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) return wnkpc @@ -281,16 +281,16 @@ def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): def mkt_clearing(p, A, B, Bg, vphi, muw, tax, w, U): wealth = A + B asset_mkt = p + Bg - wealth - labor_mkt = vphi - muw * (1 - tax) * w * U + labor_mkt = vphi - (1 - tax) * w * U / muw return asset_mkt, labor_mkt, wealth @simple -def mkt_clearing_all(p, A, B, Bg, vphi, muw, tax, w, U, C, I, G, Chi, omega): +def mkt_clearing_all(p, A, B, Bg, vphi, muw, tax, w, U, C, I, G, Chi, omega, psip): wealth = A + B - asset_mkt = p + Bg - wealth - labor_mkt = vphi - muw * (1 - tax) * w * U - goods_mkt = C + I + G + Chi + omega * B - 1 + asset_mkt = p + Bg - B - A + labor_mkt = vphi - (1 - tax) * w * U / muw + goods_mkt = C + I + G + Chi + psip + omega * B - 1 return asset_mkt, labor_mkt, goods_mkt, wealth @@ -375,7 +375,7 @@ def res(x): out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) asset_mkt = out['A'] + out['B'] - p - Bg - labor_mkt = vphi_loc - muw * (1 - tax) * w * out['U'] + labor_mkt = vphi_loc - (1 - tax) * w * out['U'] / muw return np.array([asset_mkt, labor_mkt, out['B'] - Bh]) # solve for beta, vphi, omega @@ -400,11 +400,11 @@ def res(x): 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'z_grid': z_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, - 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psiw': 0, 'psip': 0, 'inv': 0, - 'labor_mkt': vphi - muw * (1 - tax) * w * ss["U"], + 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, + 'labor_mkt': vphi - (1 - tax) * w * ss["U"] / muw, 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1/mup), - 'wnkpc': kappaw * (vphi * ss["N"]**(1+1/frisch) - muw*(1-tax)*w*ss["N"]*ss["U"]), + 'wnkpc': kappaw * (vphi * ss["N"]**(1+1/frisch) - (1-tax)*w*ss["N"]*ss["U"]/muw), 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1-alpha) * mc - delta - r}) return ss From 2031e3050eb76f1692eb16265b31e460e2772d9b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 15:50:16 -0500 Subject: [PATCH 097/288] Fix bug w/ block_sort. topsorted are indices referencing the blocks passed in, so constructing blocks_all w/ self.blocks but passing in self._blocks_unsorted is inconsistent. --- sequence_jacobian/blocks/combined_block.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 6b02d0a..e45a0b3 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -60,7 +60,7 @@ def steady_state(self, calibration, helper_blocks=None, **kwargs): if helper_blocks is None: helper_blocks = [] - topsorted = utils.graph.block_sort(self._blocks_unsorted, calibration=calibration, helper_blocks=helper_blocks) + topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) blocks_all = self.blocks + helper_blocks ss_partial_eq = deepcopy(calibration) From c8b95d550a049ce2cc3b0c0f93291d8ed5ce4233 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 5 Apr 2021 15:51:00 -0500 Subject: [PATCH 098/288] Use the JacobianDict's pandas-like indexing to index out relevant exogenous (inputs) in JacobianDictBlock --- .../blocks/auxiliary_blocks/jacobiandict_block.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index afcdc85..227779d 100644 --- a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -23,15 +23,4 @@ def jacobian(self, exogenous: List[str] = None, **kwargs) -> JacobianDict: if exogenous is None: return JacobianDict(self.nesteddict) else: - if self.inputs == exogenous: - return JacobianDict(self.nesteddict) - else: - nesteddict_subset = {} - for o in self.nesteddict.keys(): - for i in self.nesteddict[o].keys(): - if i in exogenous: - if o in nesteddict_subset: - nesteddict_subset[o][i] = self.nesteddict[o][i] - else: - nesteddict_subset[o] = {i: self.nesteddict[o][i]} - return JacobianDict(nesteddict_subset) + return self[:, exogenous] From 82ee5658e0248a7b7e24f4ff492126d2c3f259cf Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 7 Apr 2021 12:16:16 -0500 Subject: [PATCH 099/288] Fix implementation of RBC two-asset w new helpers. --- requirements.txt | 11 +- sequence_jacobian/models/rbc.py | 28 +++-- sequence_jacobian/models/two_asset.py | 144 +++++++++++------------ tests/base/test_transitional_dynamics.py | 8 +- tests/base/test_two_asset.py | 7 +- tests/conftest.py | 28 ++--- tests/robustness/test_steady_state.py | 3 +- 7 files changed, 115 insertions(+), 114 deletions(-) diff --git a/requirements.txt b/requirements.txt index 854e244..55c1367 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ -numpy -scipy -numba -xarray +numpy~=1.18.5 +scipy~=1.5.0 +numba~=0.50.1 +xarray~=0.17.0 + +pytest~=5.4.3 +setuptools~=49.2.0 \ No newline at end of file diff --git a/sequence_jacobian/models/rbc.py b/sequence_jacobian/models/rbc.py index 3268613..465a54f 100644 --- a/sequence_jacobian/models/rbc.py +++ b/sequence_jacobian/models/rbc.py @@ -24,26 +24,32 @@ def household(K, L, w, eis, frisch, vphi, delta): def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): goods_mkt = Y - C - I euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - walras = C + K - (1 + r) * K(-1) - w * L # we can the check dynamic version too + walras = C + K - (1 + r) * K(-1) - w * L return goods_mkt, euler, walras @simple -def steady_state_solution(r, eis, delta, alpha): - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * K ** alpha - I = delta * K - C = Y - I +def steady_state_solution(Y, L, r, eis, delta, alpha): + # 1. Solve for beta to hit r beta = 1 / (1 + r) - vphi = w * C ** (-1 / eis) + # 2. Solve for K to hit goods_mkt + K = alpha * Y / (r + delta) + w = (1 - alpha) * Y / L + C = w * L + (1 + r) * K(-1) - K + I = delta * K goods_mkt = Y - C - I + + # 3. Solve for Z to hit Y + Z = Y * K ** (-alpha) * L ** (alpha - 1) + + # 4. Solve for vphi to hit L + vphi = w * C ** (-1 / eis) + + # 5. Have to return euler because it's a target euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - return Z, K, Y, w, I, C, beta, vphi, goods_mkt, euler + return beta, K, w, C, I, goods_mkt, Z, vphi, euler '''Part 2: Steady state''' diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index ee1ff6a..6b6b266 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -28,14 +28,12 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei Psi1 = get_Psi_and_deriv(a_grid[:, np.newaxis], a_grid[np.newaxis, :], ra, chi0, chi1, chi2)[1] - # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === # (take discounted expectation of tomorrow's value function) - Wb = matrix_times_first_dim(beta*Pi_p, Vb_p) - Wa = matrix_times_first_dim(beta*Pi_p, Va_p) + Wb = matrix_times_first_dim(beta * Pi_p, Vb_p) + Wa = matrix_times_first_dim(beta * Pi_p, Va_p) W_ratio = Wa / Wb - # === STEP 3: a'(z, b', a) for UNCONSTRAINED === # for each (z, b', a), linearly interpolate to find a' between gridpoints @@ -49,8 +47,8 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei # === STEP 4: b'(z, b, a), a'(z, b, a) for UNCONSTRAINED === # solve out budget constraint to get b(z, b', a) - b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1+ra)*a_grid) - + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) # interpolate this b' -> b mapping to get b -> b', so we have b'(z, b, a) # and also use interpolation to get a'(z, b, a) @@ -60,7 +58,6 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei a_unc = utils.interpolate.apply_coord(i, pi, a_endo_unc.swapaxes(1, 2)).swapaxes(1, 2) b_unc = utils.interpolate.apply_coord(i, pi, b_grid).swapaxes(1, 2) - # === STEP 5: a'(z, kappa, a) for CONSTRAINED === # for each (z, kappa, a), linearly interpolate to find a' between gridpoints @@ -70,16 +67,15 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei # use same interpolation to get Wb and then c a_endo_con = utils.interpolate.apply_coord(i, pi, a_grid) - c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis])**(-eis) - * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) - + c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis]) ** (-eis) + * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) # === STEP 6: a'(z, b, a) for CONSTRAINED === # solve out budget constraint to get b(z, kappa, a), enforcing b'=0 b_endo = (c_endo_con + a_endo_con - + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1+ra)*a_grid) - + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) # interpolate this kappa -> b mapping to get b -> kappa # then use the interpolated kappa to get a', so we have a'(z, b, a) @@ -88,7 +84,6 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei a_con = utils.interpolate.interpolate_y(b_endo.swapaxes(1, 2), b_grid, a_endo_con.swapaxes(1, 2)).swapaxes(1, 2) - # === STEP 7: obtain policy functions and update derivatives of value function === # combine unconstrained solution and constrained solution, choosing latter @@ -101,7 +96,7 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei Psi, _, Psi2 = get_Psi_and_deriv(a, a_grid, ra, chi0, chi1, chi2) # solve out budget constraint to get consumption and marginal utility - c = addouter(z_grid, (1+rb)*b_grid, (1+ra)*a_grid) - Psi - a - b + c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b uc = c ** (-1 / eis) # for GE wage Phillips curve we'll need endowment-weighted utility too @@ -119,9 +114,6 @@ def income(e_grid, tax, w, N): return z_grid -household.add_hetinput(income, verbose=False) - - # A potential hetoutput to include with the above HetBlock @hetoutput() def adjustment_costs(a, a_grid, r, chi0, chi1, chi2): @@ -129,8 +121,13 @@ def adjustment_costs(a, a_grid, r, chi0, chi1, chi2): return chi +household.add_hetinput(income, verbose=False) +household.add_hetoutput(adjustment_costs, verbose=False) + + """Supporting functions for HA block""" + def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): """Adjustment cost Psi(ap, a) and its derivatives with respect to first argument (ap) and second argument (a)""" @@ -144,7 +141,7 @@ def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): Psi = chi1 / chi2 * abs_a_change * core_factor Psi1 = chi1 * sign_change * core_factor - Psi2 = -(1 + ra)*(Psi1 + (chi2 - 1)*Psi/adj_denominator) + Psi2 = -(1 + ra) * (Psi1 + (chi2 - 1) * Psi / adj_denominator) return Psi, Psi1, Psi2 @@ -198,9 +195,9 @@ def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): iout[j] = 0 piout[j] = 1 else: - iout[j] = i-1 + iout[j] = i - 1 err_upper = rhs[i, j] - lhs[i] - err_lower = rhs[i-1, j] - lhs[i-1] + err_lower = rhs[i - 1, j] - lhs[i - 1] piout[j] = err_upper / (err_upper - err_lower) @@ -209,7 +206,7 @@ def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): @simple def pricing(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1/mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log)\ + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) \ / (1 + r(+1)) - (1 + pi).apply(np.log) return nkpc @@ -229,9 +226,9 @@ def labor(Y, w, K, Z, alpha): @simple def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): - inv = (K/K(-1) - 1) / (delta * epsI) + 1 - Q - val = alpha * Z(+1) * (N(+1) / K) ** (1-alpha) * mc(+1) - (K(+1)/K - - (1-delta) + (K(+1)/K - 1)**2 / (2*delta*epsI)) + K(+1)/K*Q(+1) - (1 + r(+1))*Q + inv = (K / K(-1) - 1) / (delta * epsI) + 1 - Q + val = alpha * Z(+1) * (N(+1) / K) ** (1 - alpha) * mc(+1) - (K(+1) / K - (1 - delta) + (K(+1) / K - 1) ** 2 / ( + 2 * delta * epsI)) + K(+1) / K * Q(+1) - (1 + r(+1)) * Q return inv, val @@ -240,7 +237,7 @@ def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) I = K - (1 - delta) * K(-1) + k_adjust - div = Y - w * N - I - psip + div = Y - w * N - I return psip, I, div @@ -259,39 +256,30 @@ def fiscal(r, w, N, G, Bg): @simple def finance(i, p, pi, r, div, omega, pshare): rb = r - omega - ra = pshare * (div + p) / p(-1) + (1-pshare) * (1 + r) - 1 + ra = pshare(-1) * (div + p) / p(-1) + (1 - pshare(-1)) * (1 + r) - 1 fisher = 1 + i(-1) - (1 + r) * (1 + pi) return rb, ra, fisher @simple -def wage(pi, w, N, muw, kappaw): +def wage(pi, w): piw = (1 + pi) * w / w(-1) - 1 return piw @simple def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N**(1+1/frisch) - (1-tax)*w*N*U/muw) + beta *\ + wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) return wnkpc @simple -def mkt_clearing(p, A, B, Bg, vphi, muw, tax, w, U): +def mkt_clearing(p, A, B, Bg, C, I, G, Chi, psip, omega, Y): wealth = A + B asset_mkt = p + Bg - wealth - labor_mkt = vphi - (1 - tax) * w * U / muw - return asset_mkt, labor_mkt, wealth - - -@simple -def mkt_clearing_all(p, A, B, Bg, vphi, muw, tax, w, U, C, I, G, Chi, omega, psip): - wealth = A + B - asset_mkt = p + Bg - B - A - labor_mkt = vphi - (1 - tax) * w * U / muw - goods_mkt = C + I + G + Chi + psip + omega * B - 1 - return asset_mkt, labor_mkt, goods_mkt, wealth + goods_mkt = C + I + G + Chi + psip + omega * B - Y + return asset_mkt, wealth, goods_mkt @simple @@ -311,34 +299,45 @@ def share_value(p, tot_wealth, Bh): @simple -def partial_steady_state_solution(delta, K, r, tot_wealth, Bh, Bg, G, omega): - I = delta * K - mc = 1 - r * (tot_wealth - Bg - K) - alpha = (r + delta) * K / mc +def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): + """Solves for (mup, alpha, Z) to hit (tot_wealth, Y, K).""" + # 1. Solve for markup to hit total wealth + p = tot_wealth - Bg + mc = 1 - r * (p - K) / Y mup = 1 / mc - Z = K ** (-alpha) - w = (1 - alpha) * mc - tax = (r * Bg + G) / w - div = 1 - w - I - p = div / r - ra = r - rb = r - omega - pshare = p / (tot_wealth - Bh) - - # TODO: These are completely wrong...but left as is. Change these please! - N = (1. / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) - wnkpc = 0. wealth = tot_wealth - return I, mc, alpha, mup, Z, w, tax, div, p, ra, rb, pshare, N, wnkpc, wealth + # 2. Solve for capital share to hit K + alpha = (r + delta) * K / Y / mc + + # 3. Solve for TFP to hit N (Y cannot be used, because it is an unknown of the DAG) + Z = Y * K ** (-alpha) * N ** (alpha - 1) + + # 4. Solve for w such that piw = 0 + w = mc * (1 - alpha) * Y / N + piw = 0 + + return p, mc, mup, wealth, alpha, Z, w, piw + + +"""HA: solve for (beta, chi1) to hit (B, asset_mkt).""" + + +@simple +def partial_ss_step2(tax, w, U, N, muw, frisch): + """Solves for (vphi) to hit (wnkpc).""" + vphi = (1 - tax) * w * U / muw * N ** (-1 / frisch) + wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw + return vphi, wnkpc '''Part 3: Steady state''' -def two_asset_ss(beta_guess=0.976, vphi_guess=2.07, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, +def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi2=2, epsI=4, omega=0.005, kappaw=0.1, - phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, verbose=True): + phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, + verbose=True): """Solve steady state of full GE model. Calibrate (beta, vphi, chi1, alpha, mup, Z) to hit targets for (r, tot_wealth, Bh, K, Y=N=1). """ @@ -369,50 +368,49 @@ def two_asset_ss(beta_guess=0.976, vphi_guess=2.07, chi1_guess=6.5, r=0.0125, to # residual function def res(x): - beta_loc, vphi_loc, chi1_loc = x - if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001 or chi1_loc < 0.5: + beta_loc, chi1_loc = x + if beta_loc > 0.999 / (1 + r) or chi1_loc < 0.5: raise ValueError('Clearly invalid inputs') out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) asset_mkt = out['A'] + out['B'] - p - Bg - labor_mkt = vphi_loc - (1 - tax) * w * out['U'] / muw - return np.array([asset_mkt, labor_mkt, out['B'] - Bh]) + return np.array([asset_mkt, out['B'] - Bh]) # solve for beta, vphi, omega - (beta, vphi, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess, chi1_guess]), - verbose=verbose) + (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), verbose=verbose) # extra evaluation to report variables ss = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, k_grid=k_grid, beta=beta, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1, chi2=chi2) # other things of interest + vphi = (1 - tax) * w * ss['U'] / muw pshare = p / (tot_wealth - Bh) # calculate aggregate adjustment cost and check Walras's law chi = get_Psi_and_deriv(ss['a'], a_grid, r, chi0, chi1, chi2)[0] Chi = np.vdot(ss['D'], chi) goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 - assert np.abs(goods_mkt) < 1E-7 ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, - 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'chi': chi, 'Chi': Chi, 'phi': phi, + 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'chi': chi, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'z_grid': z_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, - 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, - 'labor_mkt': vphi - (1 - tax) * w * ss["U"] / muw, + 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, 'goods_mkt': goods_mkt, 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], - 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1/mup), - 'wnkpc': kappaw * (vphi * ss["N"]**(1+1/frisch) - (1-tax)*w*ss["N"]*ss["U"]/muw), - 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1-alpha) * mc - delta - r}) + 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1 / mup), + 'wnkpc': kappaw * (vphi * ss["N"] ** (1 + 1 / frisch) - (1 - tax) * w * ss["N"] * ss["U"] / muw), + 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1 - alpha) * mc - delta - r}) return ss '''Part 4: Solved blocks for transition dynamics/Jacobian calculation''' + + @solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver="brentq") def pricing_solved(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1/mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ (1 + r(+1)) - (1 + pi).apply(np.log) return nkpc diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 4d04517..0ef1c10 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -88,11 +88,9 @@ def test_two_asset_td(two_asset_hank_dag): def test_two_asset_solved_v_simple_td(two_asset_hank_dag): two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag - household = copy.deepcopy(two_asset.household) - household.add_hetoutput(two_asset.adjustment_costs, verbose=False) - blocks_simple = [household, two_asset.make_grids, + blocks_simple = [two_asset.household, two_asset.make_grids, two_asset.pricing, two_asset.arbitrage, two_asset.labor, two_asset.investment, - two_asset.dividend, two_asset.taylor, two_asset.fiscal, + two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] two_asset_model_simple = combine(blocks_simple, name="Two-Asset HANK w/ SimpleBlocks") unknowns_simple = ["r", "w", "Y", "pi", "p", "Q", "K"] @@ -115,4 +113,4 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) assert np.linalg.norm(dY_nonlin - dY_nonlin_simple, np.inf) < 2e-7 - assert np.linalg.norm(dY - dY_simple, np.inf) < 0.02 \ No newline at end of file + assert np.linalg.norm(dY - dY_simple, np.inf) < 0.02 diff --git a/tests/base/test_two_asset.py b/tests/base/test_two_asset.py index ca99897..1454e5c 100644 --- a/tests/base/test_two_asset.py +++ b/tests/base/test_two_asset.py @@ -13,10 +13,9 @@ def test_hank_ss(): assert np.isclose(U, 4.5102870939550055) -def hank_ss_singlerun(beta=0.976, vphi=2.07, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, - muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi1=6.5, chi2=2, - epsI=4, omega=0.005, kappaw=0.1, phi=1.5, nZ=3, nB=50, nA=70, nK=50, - bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, verbose=True): +def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg=2.8, G=0.2, eis=0.5, + chi0=0.25, chi1=6.5, chi2=2, omega=0.005, nZ=3, nB=50, nA=70, nK=50, + bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92): """Mostly cribbed from two_asset.hank_ss(), but just does backward iteration to get a partial equilibrium household steady state given parameters, not solving for equilibrium. Convenient for testing.""" diff --git a/tests/conftest.py b/tests/conftest.py index 22862dd..46576a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ """Fixtures used by tests.""" import pytest -import copy from sequence_jacobian import create_model from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset @@ -14,9 +13,9 @@ def rbc_dag(): rbc_model = create_model(blocks, name="RBC") # Steady State - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "frisch": 1., "L": 1.0} - unknowns_ss = {"beta": 0.98, "vphi": 0.95, "Z": 1., "K": 3.} - targets_ss = {"goods_mkt": 0, "euler": 0, "Y": 1., "r": 0.01} + calibration = {'L': 1.0, "eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11} + unknowns_ss = {'K': 3., "Z": 1., "beta": 0.99, 'vphi': 0.92} + targets_ss = {'goods_mkt': 0., 'Y': 1., 'euler': 0., 'r': 0.01} ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="solved") @@ -77,23 +76,20 @@ def one_asset_hank_dag(): @pytest.fixture(scope='session') def two_asset_hank_dag(): - household = copy.deepcopy(two_asset.household) - household.add_hetoutput(two_asset.adjustment_costs, verbose=False) - blocks = [household, two_asset.make_grids, + blocks = [two_asset.household, two_asset.make_grids, two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved, - two_asset.dividend, two_asset.taylor, two_asset.fiscal, + two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] - helper_blocks = [two_asset.partial_steady_state_solution] + helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step1] two_asset_model = create_model(blocks, name="Two-Asset HANK") # Steady State - calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, - "tot_wealth": 14, "K": 10, "delta": 0.02, "kappap": 0.1, "muw": 1.1, "Bh": 1.04, - "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, - "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, - "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - unknowns_ss = {"beta": 0.976, "vphi": 2.07, "chi1": 6.5} - targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} + calibration = {"Y": 1., "r": 0.0125, "rstar": 0.0125, "tot_wealth": 14, "delta": 0.02, "kappap": 0.1, "muw": 1.1, + "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, + "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, + "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} + unknowns_ss = {"beta": 0.976, "vphi": 1.71, "chi1": 6.5, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} + targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="broyden_custom") diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index 312aaac..070872d 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -5,6 +5,7 @@ from sequence_jacobian.models import hank, two_asset + # Filter out warnings when the solver is trying to search in bad regions @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): @@ -15,7 +16,7 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} - unknowns = {"beta": (0.95, 0.97, 0.999/(1 + 0.005)), "vphi": (0.001, 1.0, 10)} + unknowns = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10)} targets = {"asset_mkt": 0, "labor_mkt": 0} ss_ref = hank_model.solve_steady_state(calibration, unknowns, targets, helper_blocks=helper_blocks, solver="broyden1", solver_kwargs={"options": {"maxiter": 250}}, From 013690d503375daa9fb97a111cc8ad59935882b9 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 7 Apr 2021 17:17:58 -0500 Subject: [PATCH 100/288] Caught another implicit L=1 in RBC; two_asset.partial_ss_step2 does not work as intended! It was not called before because of a typo. --- sequence_jacobian/models/rbc.py | 4 ++-- sequence_jacobian/models/two_asset.py | 20 ++++++++++---------- tests/conftest.py | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sequence_jacobian/models/rbc.py b/sequence_jacobian/models/rbc.py index 465a54f..61db8b0 100644 --- a/sequence_jacobian/models/rbc.py +++ b/sequence_jacobian/models/rbc.py @@ -29,7 +29,7 @@ def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): @simple -def steady_state_solution(Y, L, r, eis, delta, alpha): +def steady_state_solution(Y, L, r, eis, delta, alpha, frisch): # 1. Solve for beta to hit r beta = 1 / (1 + r) @@ -44,7 +44,7 @@ def steady_state_solution(Y, L, r, eis, delta, alpha): Z = Y * K ** (-alpha) * L ** (alpha - 1) # 4. Solve for vphi to hit L - vphi = w * C ** (-1 / eis) + vphi = w * C ** (-1 / eis) * L ** (-1 / frisch) # 5. Have to return euler because it's a target euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 6b6b266..68df164 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -300,7 +300,7 @@ def share_value(p, tot_wealth, Bh): @simple def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): - """Solves for (mup, alpha, Z) to hit (tot_wealth, Y, K).""" + """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" # 1. Solve for markup to hit total wealth p = tot_wealth - Bg mc = 1 - r * (p - K) / Y @@ -315,20 +315,20 @@ def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): # 4. Solve for w such that piw = 0 w = mc * (1 - alpha) * Y / N - piw = 0 + pi = 0 - return p, mc, mup, wealth, alpha, Z, w, piw + return p, mc, mup, wealth, alpha, Z, w, pi """HA: solve for (beta, chi1) to hit (B, asset_mkt).""" - -@simple -def partial_ss_step2(tax, w, U, N, muw, frisch): - """Solves for (vphi) to hit (wnkpc).""" - vphi = (1 - tax) * w * U / muw * N ** (-1 / frisch) - wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw - return vphi, wnkpc +# TODO: this does not work, probably uses wrong U +# @simple +# def partial_ss_step2(tax, w, U, N, muw, frisch): +# """Solves for (vphi) to hit (wnkpc).""" +# vphi = (1 - tax) * w * U / muw * N ** (-1 / frisch) +# wnkpc = vphi * N ** (1 / frisch) - (1 - tax) * w * U / muw +# return vphi, wnkpc '''Part 3: Steady state''' diff --git a/tests/conftest.py b/tests/conftest.py index 46576a3..ff66713 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,7 +80,7 @@ def two_asset_hank_dag(): two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved, two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] - helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step1] + helper_blocks = [two_asset.partial_ss_step1] two_asset_model = create_model(blocks, name="Two-Asset HANK") # Steady State @@ -89,7 +89,7 @@ def two_asset_hank_dag(): "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns_ss = {"beta": 0.976, "vphi": 1.71, "chi1": 6.5, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} - targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} + targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, helper_blocks=helper_blocks, solver="broyden_custom") From 9b80db462428d6419a0b2e6f683d551c869c1364 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Apr 2021 09:15:23 -0500 Subject: [PATCH 101/288] Change parent class name from LinearOperator to Jacobian for Jacobian-like types --- sequence_jacobian/jacobian/classes.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 9b677cb..7759671 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -7,16 +7,16 @@ from . import support -class LinearOperator(metaclass=ABCMeta): +class Jacobian(metaclass=ABCMeta): """An abstract base class encompassing all valid types representing Jacobians, which include np.ndarray, IdentityMatrix, ZeroMatrix, and SimpleSparse.""" pass -# Make np.ndarray a child class of LinearOperator -LinearOperator.register(np.ndarray) +# Make np.ndarray a child class of Jacobian +Jacobian.register(np.ndarray) -class IdentityMatrix(LinearOperator): +class IdentityMatrix(Jacobian): """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, use to initialize Jacobian of a variable wrt itself""" __array_priority__ = 10_000 @@ -63,7 +63,7 @@ def __repr__(self): return 'IdentityMatrix' -class ZeroMatrix(LinearOperator): +class ZeroMatrix(Jacobian): """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, use in common case where some outputs don't depend on inputs""" __array_priority__ = 10_000 @@ -112,7 +112,7 @@ def __repr__(self): return 'ZeroMatrix' -class SimpleSparse(LinearOperator): +class SimpleSparse(Jacobian): """Efficient representation of sparse linear operators, which are linear combinations of basis operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s (measured by # above main diagonal) and m is number of initial entries missing. @@ -367,8 +367,8 @@ def deduplicate(mylist): def ensure_valid_nesteddict(d): - """The valid structure of `d` is a Dict[str, Dict[str, LinearOperator]], where calling `d[o][i]` yields a - Jacobian of type LinearOperator mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed + """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a + Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed to be {}, which is permitted the empty version of a valid nested dict.""" if d != {}: @@ -382,14 +382,14 @@ def ensure_valid_nesteddict(d): raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" f" `input` names.") jac_o_i = next(iter(jac_o_dict.values())) - if not isinstance(jac_o_i, LinearOperator): - raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `LinearOperator`.") + if not isinstance(jac_o_i, Jacobian): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") else: if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: - raise ValueError(f"The Jacobians in {d} must be square matrices of type `LinearOperator`.") + raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") else: raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" - f" values of type `LinearOperator`.") + f" values of type `Jacobian`.") From cbc9513037d98d796a7740a3d09e737042b30c83 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Apr 2021 15:03:36 -0500 Subject: [PATCH 102/288] Fix bug in solve_steady_state() for Block types, where helper_blocks was being passed in the main blocks list --- sequence_jacobian/primitives.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 6163c64..f4e05bf 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -93,8 +93,6 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] - if "helper_blocks" in kwargs and kwargs["helper_blocks"] is not None: - blocks = blocks + kwargs["helper_blocks"] solver = solver if solver else provide_solver_default(unknowns) return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) From d2056c69000286e1fe79c17ebcbcb15e18b2a5a0 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Apr 2021 15:05:48 -0500 Subject: [PATCH 103/288] Enhance steady_state to take helper_targets kwarg to explicitly specify which targets are handled and handle multiple helper blocks, some of which are non-excludable, properly --- sequence_jacobian/steady_state/drivers.py | 68 +++++++++++++---------- sequence_jacobian/steady_state/support.py | 51 +++++++++++++---- 2 files changed, 80 insertions(+), 39 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 6d82e39..d5ef9fe 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -7,12 +7,13 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ - subset_helper_block_unknowns_and_targets + subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs, find_excludable_helper_blocks from ..utilities import solvers, graph, misc # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sort_blocks=True, +def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, + helper_blocks=None, helper_targets=None, consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): @@ -27,11 +28,13 @@ def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sor A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver targets: `dict` A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG - helper_blocks: `list` - A list of blocks that replace some of the equations in the DAG to aid steady state calculation sort_blocks: `bool` Whether the blocks need to be topologically sorted (only False when this function is called from within a Block object, like CombinedBlock, that has already pre-sorted the blocks) + helper_blocks: `list` + A list of blocks that replace some of the equations in the DAG to aid steady state calculation + helper_targets: `list/dict` + A list/dict of target names (and optionally their values) that are handled by the helper blocks consistency_check: `bool` If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values without the assistance of helper blocks and see if the targets are still hit @@ -64,27 +67,25 @@ def steady_state(blocks, calibration, unknowns, targets, helper_blocks=None, sor A dictionary containing all of the pre-specified values and computed values from the steady state computation """ - # Populate otherwise mutable default arguments - if helper_blocks is None: - helper_blocks = [] - if block_kwargs is None: - block_kwargs = {} - if solver_kwargs is None: - solver_kwargs = {} - if constrained_kwargs is None: - constrained_kwargs = {} + helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ + instantiate_steady_state_mutable_kwargs(helper_blocks, helper_targets, block_kwargs, + solver_kwargs, constrained_kwargs) # Initial setup of blocks, targets, and dictionary of steady state values to be returned blocks_all = blocks + helper_blocks targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - - helper_unknowns, helper_targets = subset_helper_block_unknowns_and_targets(helper_blocks, unknowns, targets) + helper_targets = {t: targets[t] for t in targets if t in helper_targets} if isinstance(helper_targets, list) else helper_targets ss_values = deepcopy(calibration) ss_values.update(helper_targets) + helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) + helper_indices = np.arange(len(blocks), len(blocks_all)) + if sort_blocks: - topsorted = graph.block_sort(blocks, calibration=helper_targets, helper_blocks=helper_blocks) + dep = graph.construct_dependency_graph(blocks, graph.construct_output_map(blocks, helper_blocks=helper_blocks), + calibration=ss_values, helper_blocks=helper_blocks) + topsorted = graph.topological_sort(dep) else: topsorted = range(len(blocks + helper_blocks)) @@ -121,7 +122,8 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): if helper_blocks: unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, - helper_targets, solver, solver_kwargs, + helper_targets, helper_indices, blocks_all, dep, + solver, solver_kwargs, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=ttol, verbose=verbose, fragile=fragile) @@ -254,31 +256,39 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, + helper_indices, blocks_all, block_dependencies, solver, solver_kwargs, constrained_method="linear_continuation", constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" - # Initial verification that helper block targets are satisfied by the helper blocks + # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, + # to populate the `ss_values` dict with the unknown values that: + # a) are handled by helper blocks and b) are excludable from the main DAG. unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) - # Subset out the unknowns and targets that are not handled by helper blocks - unknowns_w_o_helpers = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), helper_unknowns)} - targets_w_o_helpers = misc.dict_diff(targets, helper_targets) + # Find the unknowns and targets that are both handled by helper blocks and are excludable from the main DAG + # evaluation by checking block dependencies + unknowns_excl, targets_excl = find_excludable_helper_blocks(blocks_all, block_dependencies, + helper_indices, helper_unknowns, helper_targets) + + # Subset out the unknowns and targets that are not excludable from the main DAG loop + unknowns_non_excl = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), unknowns_excl)} + targets_non_excl = misc.dict_diff(targets, targets_excl) - # Assumption: If the targets handled by helpers are satisfied under the provided - # set of unknown variables' initial values then it is assumed they will be under other unknown variables' - # initial values and hence the unknowns/targets handled by helpers and the helper blocks themselves - # can be omitted from the DAG when solving. - if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): - unknown_solutions = _solve_for_unknowns(residual, unknowns_w_o_helpers, targets_w_o_helpers, + # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of + # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and + # we can omit them and their corresponding `unknowns` in the main DAG. + if np.all(np.isclose([targets_init_vals[t] for t in targets_excl.keys()], 0.)): + unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, solver, solver_kwargs, residual_kwargs={"include_helpers": False}, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=tol, verbose=verbose, fragile=fragile) - # If targets handled by helpers are not satisfied then it is assumed that helper blocks merely aid in providing - # more accurate guesses for the DAG solution and they remain a part of the DAG when solving. + # If targets handled by helpers and excludable from the main DAG are not satisfied then + # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, + # and they remain a part of the main DAG when solving. else: unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, constrained_method=constrained_method, diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index 4b51609..6860214 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -5,6 +5,28 @@ import numpy as np +def instantiate_steady_state_mutable_kwargs(helper_blocks, helper_targets, block_kwargs, solver_kwargs, + constrained_kwargs): + """Instantiate mutable types from `None` default values in the steady_state function""" + if helper_blocks is None and helper_targets is None: + helper_blocks = [] + helper_targets = [] + elif helper_blocks is not None and helper_targets is None: + raise ValueError("If the user has provided `helper_blocks`, the kwarg `helper_targets` must be specified" + " indicating which target variables are handled by the `helper_blocks`.") + elif helper_blocks is None and helper_targets is not None: + raise ValueError("If the user has provided `helper_targets`, the kwarg `helper_blocks` must be specified" + " indicating which helper blocks handle the `helper_targets`") + if block_kwargs is None: + block_kwargs = {} + if solver_kwargs is None: + solver_kwargs = {} + if constrained_kwargs is None: + constrained_kwargs = {} + + return helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs + + def provide_solver_default(unknowns): if len(unknowns) == 1: bounds = list(unknowns.values())[0] @@ -71,23 +93,32 @@ def compute_target_values(targets, potential_args): return target_values -def subset_helper_block_unknowns_and_targets(helper_blocks, unknowns, targets): - """Find the set of unknowns and targets that the `helper_blocks` solve out""" - unknowns_handled_by_helpers = set() - targets_handled_by_helpers = set() +def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): + """Find the set of unknowns that the `helper_blocks` solve for""" + unknowns_handled_by_helpers = {} for block in helper_blocks: - unknowns_handled_by_helpers |= (block.inputs | block.outputs) & set(unknowns.keys()) - targets_handled_by_helpers |= (block.inputs | block.outputs) & set(targets.keys()) - unknowns_handled_by_helpers = list(unknowns_handled_by_helpers) - targets_handled_by_helpers = list(targets_handled_by_helpers) + unknowns_handled_by_helpers.update({u: unknowns_all[u] for u in block.outputs if u in unknowns_all}) n_unknowns = len(unknowns_handled_by_helpers) - n_targets = len(targets_handled_by_helpers) + n_targets = len(helper_targets) if n_unknowns != n_targets: raise ValueError(f"The provided helper_blocks handle {n_unknowns} unknowns != {n_targets} targets." f" User must specify an equal number of unknowns/targets solved for by helper blocks.") - return unknowns_handled_by_helpers, dict(zip(targets_handled_by_helpers, [targets[t] for t in targets_handled_by_helpers])) + return unknowns_handled_by_helpers + + +def find_excludable_helper_blocks(blocks_all, block_dependencies, helper_indices, helper_unknowns, helper_targets): + """Of the set of helper_unknowns and helper_targets, find the ones that can be excluded from the main DAG + for the purposes of numerically solving unknowns.""" + excludable_helper_unknowns = {} + excludable_helper_targets = {} + for i in helper_indices: + # If the helper block has no dependencies on other blocks in the DAG + if not block_dependencies[i]: + excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) + excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) + return excludable_helper_unknowns, excludable_helper_targets def extract_univariate_initial_values_or_bounds(unknowns): From 7af22aa56e68d1fe02bae955d43296090187f4b0 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Apr 2021 15:06:37 -0500 Subject: [PATCH 104/288] Update tests to work with new helper_targets convention --- sequence_jacobian/models/two_asset.py | 19 ++++++--------- tests/conftest.py | 29 ++++++++++++---------- tests/robustness/test_steady_state.py | 35 +++++++++++++++------------ 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 68df164..9cd64e5 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -315,20 +315,17 @@ def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): # 4. Solve for w such that piw = 0 w = mc * (1 - alpha) * Y / N - pi = 0 + piw = 0 - return p, mc, mup, wealth, alpha, Z, w, pi + return p, mc, mup, wealth, alpha, Z, w, piw -"""HA: solve for (beta, chi1) to hit (B, asset_mkt).""" - -# TODO: this does not work, probably uses wrong U -# @simple -# def partial_ss_step2(tax, w, U, N, muw, frisch): -# """Solves for (vphi) to hit (wnkpc).""" -# vphi = (1 - tax) * w * U / muw * N ** (-1 / frisch) -# wnkpc = vphi * N ** (1 / frisch) - (1 - tax) * w * U / muw -# return vphi, wnkpc +@simple +def partial_ss_step2(tax, w, U, N, muw, frisch): + """Solves for (vphi) to hit (wnkpc).""" + vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) + wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw + return vphi, wnkpc '''Part 3: Steady state''' diff --git a/tests/conftest.py b/tests/conftest.py index ff66713..9925c20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,11 +13,12 @@ def rbc_dag(): rbc_model = create_model(blocks, name="RBC") # Steady State - calibration = {'L': 1.0, "eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11} - unknowns_ss = {'K': 3., "Z": 1., "beta": 0.99, 'vphi': 0.92} - targets_ss = {'goods_mkt': 0., 'Y': 1., 'euler': 0., 'r': 0.01} - ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, solver="solved") + calibration = {"eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11} + unknowns_ss = {"vphi": 0.92, "beta": 1 / (1 + 0.01), "K": 2., "Z": 1.} + targets_ss = {"L": 1., "r": 0.01, "euler": 0., "Y": 1.} + ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="solved", + helper_blocks=helper_blocks, + helper_targets=["L", "r", "euler", "Y"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] @@ -39,8 +40,8 @@ def krusell_smith_dag(): "nS": 2, "nA": 10, "amax": 200, "r": 0.01} unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} - ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, solver="brentq") + ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", + helper_blocks=helper_blocks, helper_targets=["Y", "r"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] @@ -63,8 +64,8 @@ def one_asset_hank_dag(): "pi": 0, "nS": 2, "amax": 150, "nA": 10} unknowns_ss = {"beta": 0.986, "vphi": 0.8, "w": 0.8} targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} - ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, solver="broyden_custom") + ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", + helper_blocks=helper_blocks, helper_targets=["nkpc_res"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z"] @@ -80,7 +81,7 @@ def two_asset_hank_dag(): two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved, two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] - helper_blocks = [two_asset.partial_ss_step1] + helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2] two_asset_model = create_model(blocks, name="Two-Asset HANK") # Steady State @@ -88,10 +89,12 @@ def two_asset_hank_dag(): "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - unknowns_ss = {"beta": 0.976, "vphi": 1.71, "chi1": 6.5, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} + unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} - ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, solver="broyden_custom") + ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", + helper_blocks=helper_blocks, + helper_targets={'wnkpc': 0., 'pi': 0.0, "K": 10., + "wealth": "tot_wealth", "N": 1.0}) # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z", "G"] diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index 070872d..adc4d02 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -13,13 +13,14 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): helper_blocks = [hank.partial_steady_state_solution] - calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "mu": 1.2, "B_Y": 5.6, + calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B_Y": 5.6, "mu": 1.2, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} - unknowns = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10)} - targets = {"asset_mkt": 0, "labor_mkt": 0} - ss_ref = hank_model.solve_steady_state(calibration, unknowns, targets, helper_blocks=helper_blocks, - solver="broyden1", solver_kwargs={"options": {"maxiter": 250}}, + unknowns_ss = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10.), "w": 0.8} + targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} + ss_ref = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + helper_blocks=helper_blocks, helper_targets=["nkpc_res"], + solver="broyden1", solver_kwargs={"options": {"maxiter": 300}}, constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) for k in ss.keys(): @@ -30,16 +31,18 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag): two_asset_model, _, _, _, ss = two_asset_hank_dag - helper_blocks = [two_asset.partial_steady_state_solution] - - calibration = {"pi": 0, "piw": 0, "Q": 1, "Y": 1, "N": 1, "r": 0.0125, "rstar": 0.0125, "i": 0.0125, - "tot_wealth": 14, "K": 10, "delta": 0.02, "kappap": 0.1, "muw": 1.1, "Bh": 1.04, - "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, - "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, - "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - unknowns = {"beta": (0.5, 0.9, 0.999 / (1 + 0.0125)), "vphi": (0.001, 1.0, 10.), "chi1": (0.5, 5.5, 10.)} - targets = {"asset_mkt": 0, "labor_mkt": 0, "B": "Bh"} - ss_ref = two_asset_model.solve_steady_state(calibration, unknowns, targets, helper_blocks=helper_blocks, - solver="broyden_custom", consistency_check=True) + helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2] + + # Steady State + calibration = {"Y": 1., "r": 0.0125, "rstar": 0.0125, "tot_wealth": 14, "delta": 0.02, "kappap": 0.1, "muw": 1.1, + "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, + "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, + "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} + unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} + targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} + ss_ref = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", + helper_blocks=helper_blocks, + helper_targets={'wnkpc': 0., 'pi': 0.0, "K": 10., + "wealth": "tot_wealth", "N": 1.0}) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) From 3e9dc86da4c2b853d7a3f8978d3579c499d85959 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Apr 2021 16:03:25 -0500 Subject: [PATCH 105/288] Fix bug with rbc not taking goods_mkt as a target --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9925c20..47553f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,12 +13,12 @@ def rbc_dag(): rbc_model = create_model(blocks, name="RBC") # Steady State - calibration = {"eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11} + calibration = {"eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11, "L": 1.} unknowns_ss = {"vphi": 0.92, "beta": 1 / (1 + 0.01), "K": 2., "Z": 1.} - targets_ss = {"L": 1., "r": 0.01, "euler": 0., "Y": 1.} + targets_ss = {"goods_mkt": 0., "r": 0.01, "euler": 0., "Y": 1.} ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="solved", helper_blocks=helper_blocks, - helper_targets=["L", "r", "euler", "Y"]) + helper_targets=["goods_mkt", "r", "euler", "Y"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] From 99535ca8889b7b4d37b9901a90aa8c4bde4ce9bf Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 15 Apr 2021 09:57:42 -0500 Subject: [PATCH 106/288] Change ensure_valid_nesteddict to ensure_valid_jacobiandict --- sequence_jacobian/jacobian/classes.py | 58 ++++++++++++++------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py index 7759671..b522f7d 100644 --- a/sequence_jacobian/jacobian/classes.py +++ b/sequence_jacobian/jacobian/classes.py @@ -279,7 +279,6 @@ def __init__(self, nesteddict, outputs=None, inputs=None, name=None): self.inputs = nesteddict.inputs self.name = nesteddict.name else: - ensure_valid_nesteddict(nesteddict) self.nesteddict = nesteddict if outputs is None: outputs = list(nesteddict.keys()) @@ -366,34 +365,11 @@ def deduplicate(mylist): return list(dict.fromkeys(mylist)) -def ensure_valid_nesteddict(d): - """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a - Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed - to be {}, which is permitted the empty version of a valid nested dict.""" - - if d != {}: - # Assume it's sufficient to just check one of the keys - if not isinstance(next(iter(d.keys())), str): - raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") - - jac_o_dict = next(iter(d.values())) - if isinstance(jac_o_dict, dict): - if not isinstance(next(iter(jac_o_dict.keys())), str): - raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" - f" `input` names.") - jac_o_i = next(iter(jac_o_dict.values())) - if not isinstance(jac_o_i, Jacobian): - raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") - else: - if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: - raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") - else: - raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" - f" values of type `Jacobian`.") - - - class JacobianDict(NestedDict): + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + ensure_valid_jacobiandict(nesteddict) + super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) + @staticmethod def identity(ks): return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() @@ -467,3 +443,29 @@ def unpack(bigjac, outputs, inputs, T): for iI, I in enumerate(inputs): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] return JacobianDict(jacdict, outputs, inputs) + + +def ensure_valid_jacobiandict(d): + """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a + Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed + to be {}, which is permitted the empty version of a valid nested dict.""" + + if d != {} and not isinstance(d, JacobianDict): + # Assume it's sufficient to just check one of the keys + if not isinstance(next(iter(d.keys())), str): + raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") + + jac_o_dict = next(iter(d.values())) + if isinstance(jac_o_dict, dict): + if not isinstance(next(iter(jac_o_dict.keys())), str): + raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" + f" `input` names.") + jac_o_i = next(iter(jac_o_dict.values())) + if not isinstance(jac_o_i, Jacobian): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") + else: + if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: + raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") + else: + raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" + f" values of type `Jacobian`.") From 6750a45fa9372df00a5ade4be9253e828c946f3c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 16 Apr 2021 09:07:29 -0500 Subject: [PATCH 107/288] Remove option to specify helper_targets as dict --- sequence_jacobian/steady_state/drivers.py | 6 +++--- tests/conftest.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index d5ef9fe..4e3375f 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -33,8 +33,8 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, Block object, like CombinedBlock, that has already pre-sorted the blocks) helper_blocks: `list` A list of blocks that replace some of the equations in the DAG to aid steady state calculation - helper_targets: `list/dict` - A list/dict of target names (and optionally their values) that are handled by the helper blocks + helper_targets: `list` + A list of target names that are handled by the helper blocks consistency_check: `bool` If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values without the assistance of helper blocks and see if the targets are still hit @@ -74,7 +74,7 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, # Initial setup of blocks, targets, and dictionary of steady state values to be returned blocks_all = blocks + helper_blocks targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - helper_targets = {t: targets[t] for t in targets if t in helper_targets} if isinstance(helper_targets, list) else helper_targets + helper_targets = {t: targets[t] for t in targets if t in helper_targets} ss_values = deepcopy(calibration) ss_values.update(helper_targets) diff --git a/tests/conftest.py b/tests/conftest.py index 47553f4..dfcbabc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,8 +93,7 @@ def two_asset_hank_dag(): targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", helper_blocks=helper_blocks, - helper_targets={'wnkpc': 0., 'pi': 0.0, "K": 10., - "wealth": "tot_wealth", "N": 1.0}) + helper_targets=["wnkpc", "pi", "K", "wealth", "N"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z", "G"] From c83bc35e946fc11aa0a04adab443cce3e4db8e0d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 16 Apr 2021 13:49:41 -0500 Subject: [PATCH 108/288] Remove `save=` and `use_saved=` kwargs everywhere. --- sequence_jacobian/blocks/het_block.py | 59 +++++------------------- sequence_jacobian/blocks/solved_block.py | 4 +- sequence_jacobian/jacobian/drivers.py | 28 ++++------- sequence_jacobian/nonlinear.py | 10 ++-- sequence_jacobian/utilities/misc.py | 1 + tests/base/test_transitional_dynamics.py | 22 +++++---- 6 files changed, 38 insertions(+), 86 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index b477ae2..9f234e8 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -133,12 +133,6 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non else: self.backward_init = backward_init - # 'saved' arguments start empty - self.saved = {} - self.prelim_saved = {} - self.saved_shock_list = [] - self.saved_output_list = [] - # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. def __repr__(self): @@ -247,9 +241,6 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregate_hetoutputs = {} ss.update({**hetoutputs, **aggregate_hetoutputs}) - # clear any previously saved Jacobian info for safety, since we're computing new SS - self.clear_saved() - return ss def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): @@ -312,7 +303,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal backdict = ss.copy() for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t,...] for k, v in exogenous.items()}) + backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -371,7 +362,7 @@ def impulse_linear(self, ss, exogenous, T=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h=1E-4, save=False, use_saved=False): + def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -387,11 +378,6 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= self.back_step_fun except self.back_iter_vars h : [optional] float h for numerical differentiation of backward iteration - save : [optional] bool - store curlyYs, curlyDs, curlyPs, F, and J from calculation inside HetBlock itself - useful to avoid redundant work when evaluating .jac or .ajac again - use_saved : [optional] bool - use J stored inside HetBlock to calculate the Jacobian, raises error if not available Returns ------- @@ -409,15 +395,15 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] + # TODO: get rid of this # if we're supposed to use saved Jacobian, extract T-by-T submatrices for each (o,i) - if use_saved: - return utils.misc.extract_nested_dict(savedA=self.saved['J'], - keys1=[o.capitalize() for o in outputs], - keys2=relevant_shocks, shape=(T, T)) + # if use_saved: + # return utils.misc.extract_nested_dict(savedA=self.saved['J'], + # keys1=[o.capitalize() for o in outputs], + # keys2=relevant_shocks, shape=(T, T)) # step 0: preliminary processing of steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, - sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss, save) + (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i @@ -445,10 +431,6 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) - if save: - self.saved_shock_list, self.saved_output_list = relevant_shocks, outputs - self.saved = {'curlyYs': curlyYs, 'curlyDs': curlyDs, 'curlyPs': curlyPs, 'F': F, 'J': J} - return JacobianDict(J, name=self.name) def add_hetinput(self, hetinput, overwrite=False, verbose=True): @@ -734,17 +716,15 @@ def J_from_F(F): J[1:, t] += J[:-1, t - 1] return J - '''Part 5: helpers for .jac and .ajac: preliminary processing and clearing saved info''' + '''Part 5: helpers for .jac and .ajac: preliminary processing''' - def jac_prelim(self, ss, save=False, use_saved=False): + def jac_prelim(self, ss): """Helper that does preliminary processing of steady state for fake news algorithm. Parameters ---------- ss : dict, all steady-state info, intended to be from .ss() - save : [optional] bool, whether to store results in .prelim_saved attribute - use_saved : [optional] bool, whether to use already-stored results in .prelim_saved - + Returns ---------- ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step @@ -756,14 +736,6 @@ def jac_prelim(self, ss, save=False, use_saved=False): sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy """ - output_names = ('ssin_dict', 'Pi', 'ssout_list', 'ss_for_hetinput', 'sspol_i', 'sspol_pi', 'sspol_space') - - if use_saved: - if self.prelim_saved: - return tuple(self.prelim_saved[k] for k in output_names) - else: - raise ValueError('Nothing saved to be used by jac_prelim!') - # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation ssin_dict = self.make_inputs(ss) Pi = ss[self.exogenous] @@ -784,18 +756,9 @@ def jac_prelim(self, ss, save=False, use_saved=False): sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) - if save: - self.prelim_saved = {k: v for (k, v) in zip(output_names, toreturn)} return toreturn - def clear_saved(self): - """Erase any saved Jacobian information from .jac or .ajac (e.g. if steady state changes)""" - self.saved = {} - self.prelim_saved = {} - self.saved_shock_list = [] - self.saved_output_list = [] - '''Part 6: helper to extract inputs and potentially process them through hetinput''' def make_inputs(self, back_step_inputs_dict): diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index e892160..1f4a274 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -107,7 +107,7 @@ def impulse_linear(self, ss, exogenous, T=None): targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), T=T) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, save=False, use_saved=False): + def jacobian(self, ss, exogenous=None, T=300, outputs=None): if exogenous is None: exogenous = list(self.inputs) if outputs is None: @@ -116,4 +116,4 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, save=False, use_save return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, outputs=outputs, save=save, use_saved=use_saved) + T=T, outputs=outputs) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index a9dc512..271a204 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -16,7 +16,7 @@ ''' -def get_H_U(blocks, unknowns, targets, T, ss=None, save=False, use_saved=False): +def get_H_U(blocks, unknowns, targets, T, ss=None): """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. Parameters @@ -27,8 +27,6 @@ def get_H_U(blocks, unknowns, targets, T, ss=None, save=False, use_saved=False): T : int, truncation horizon (if asymptotic, truncation horizon for backward iteration in HetBlocks) ss : [optional] dict, steady state required if blocks contains any non-jacdicts - save : [optional] bool, flag for saving Jacobians inside HetBlocks - use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks Returns ------- @@ -40,7 +38,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None, save=False, use_saved=False): """ # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, save, use_saved) + curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T) # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) @@ -49,8 +47,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None, save=False, use_saved=False): return H_U_unpacked[targets, unknowns].pack(T) -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, - H_U=None, H_U_factored=None, save=False, use_saved=False): +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_U=None, H_U_factored=None): """Get a single general equilibrium impulse response. Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed @@ -67,8 +64,6 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, outputs : [optional] list of str, variables we want impulse responses for H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) - save : [optional] bool, flag for saving Jacobians inside HetBlocks - use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks Returns ------- @@ -80,8 +75,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, T = len(x) break - curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, - save=save, use_saved=use_saved) + curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T) # step 1: if not provided, do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) if H_U is None and H_U_factored is None: @@ -116,8 +110,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, - H_U=None, H_U_factored=None, save=False, use_saved=False): +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -137,8 +130,6 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, outputs : [optional] list of str, variables we want impulse responses for H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) - save : [optional] bool, flag for saving Jacobians inside HetBlocks - use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks Returns ------- @@ -146,8 +137,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, """ # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, - save=save, use_saved=use_saved) + curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T) # step 2: do (matrix) forward accumulation to get # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) @@ -173,7 +163,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def curlyJ_sorted(blocks, inputs, ss=None, T=None, save=False, use_saved=False): +def curlyJ_sorted(blocks, inputs, ss=None, T=None): """ Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs and with respect to outputs of other blocks @@ -184,8 +174,6 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, save=False, use_saved=False): inputs : list, input names we need to differentiate with respect to ss : [optional] dict, steady state, needed if blocks includes blocks themselves T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself - save : [optional] bool, flag for saving Jacobians inside HetBlocks - use_saved : [optional] bool, flag for using saved Jacobians inside HetBlocks Returns ------- @@ -210,7 +198,7 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, save=False, use_saved=False): block = blocks[num] if hasattr(block, 'jacobian'): - jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T, "save": save, "use_saved": use_saved}.items() + jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T}.items() if k in misc.input_kwarg_list(block.jacobian)}) else: # doesn't have 'jac', must be nested dict that is jac directly diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 58edf0c..44f56d9 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -8,8 +8,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factored=None, monotonic=False, - returnindividual=False, tol=1E-8, maxit=30, verbose=True, save=False, use_saved=False, - grid_paths=None): + returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. @@ -28,9 +27,8 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factore returnindividual: [optional] bool, flag to return individual outcomes from HetBlock.td tol : [optional] scalar, for convergence of Newton's method we require |H| Date: Fri, 16 Apr 2021 16:10:52 -0500 Subject: [PATCH 109/288] Added 'Js=' kwarg to jacobian, solve_jacobian, 'impulse_linear', 'impulse_nonlinear'. --- sequence_jacobian/blocks/combined_block.py | 4 +-- sequence_jacobian/blocks/het_block.py | 25 +++++++++++------ sequence_jacobian/blocks/simple_block.py | 17 ++++++++++-- sequence_jacobian/blocks/solved_block.py | 8 +++--- sequence_jacobian/jacobian/drivers.py | 27 +++++++++--------- sequence_jacobian/nonlinear.py | 5 ++-- sequence_jacobian/utilities/misc.py | 25 ----------------- tests/base/test_transitional_dynamics.py | 32 ++++++++++++---------- 8 files changed, 69 insertions(+), 74 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index e45a0b3..eaebd52 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -92,14 +92,14 @@ def impulse_linear(self, ss, exogenous, T=None): return ImpulseDict(irf_lin_partial_eq, ss) - def jacobian(self, ss, exogenous=None, T=None, outputs=None, save=False, use_saved=False): + def jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at a steady state, `ss`""" if exogenous is None: exogenous = list(self.inputs) if outputs is None: outputs = self.outputs - kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "save": save, "use_saved": use_saved} + kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "Js": Js} for i, block in enumerate(self.blocks): curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 9f234e8..e79a207 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -353,16 +353,16 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}, ss) - def impulse_linear(self, ss, exogenous, T=None, **kwargs): + def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): # infer T from exogenous, check that all shocks have same length shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] - return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, **kwargs).apply(exogenous), ss) + return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h=1E-4): + def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -378,6 +378,8 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= self.back_step_fun except self.back_iter_vars h : [optional] float h for numerical differentiation of backward iteration + Js : [optional] dict of {str: JacobianDict}} + supply saved Jacobians Returns ------- @@ -395,12 +397,17 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] - # TODO: get rid of this - # if we're supposed to use saved Jacobian, extract T-by-T submatrices for each (o,i) - # if use_saved: - # return utils.misc.extract_nested_dict(savedA=self.saved['J'], - # keys1=[o.capitalize() for o in outputs], - # keys2=relevant_shocks, shape=(T, T)) + # if we supply Jacobians, use them if possible + if Js is not None: + # are these the Jacobians you're looking for? + if self.name in Js.keys() and isinstance(Js[self.name], JacobianDict): + J = Js[self.name] + # do they have all the inputs and outputs you need? + outputs_cap = [o.capitalize() for o in outputs] + if set(outputs_cap).issubset(set(J.outputs)) and set(relevant_shocks).issubset(set(J.inputs)): + # do they have the right length? + if T == J[J.outputs[0]][J.inputs[0]].shape[-1]: + return J # step 0: preliminary processing of steady state (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index f4b05af..6c89fd1 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -103,10 +103,10 @@ def impulse_nonlinear(self, ss, exogenous): return ImpulseDict(self._output_in_td_format(**input_args), ss) - def impulse_linear(self, ss, exogenous, T=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous), ss) + def impulse_linear(self, ss, exogenous, T=None, Js=None): + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=None): + def jacobian(self, ss, exogenous=None, T=None, Js=None): """Assemble nested dict of Jacobians Parameters @@ -118,6 +118,8 @@ def jacobian(self, ss, exogenous=None, T=None): if omitted, more efficient SimpleSparse objects returned exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs + Js : [optional] dict of {str: JacobianDict}} + supply saved Jacobians Returns ------- @@ -132,6 +134,15 @@ def jacobian(self, ss, exogenous=None, T=None): relevant_shocks = [i for i in self.inputs if i in exogenous] + # if we supply Jacobians, use them if possible + if Js is not None: + # are these the Jacobians you're looking for? + if self.name in Js.keys() and isinstance(Js[self.name], JacobianDict): + J = Js[self.name] + # do they have all the inputs you need? + if set(relevant_shocks).issubset(set(J.inputs)): + return J + # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict if not relevant_shocks: diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 1f4a274..2f37d54 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -102,12 +102,12 @@ def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - def impulse_linear(self, ss, exogenous, T=None): + def impulse_linear(self, ss, exogenous, T=None, Js=None): return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T) + T=T, Js=Js) - def jacobian(self, ss, exogenous=None, T=300, outputs=None): + def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): if exogenous is None: exogenous = list(self.inputs) if outputs is None: @@ -116,4 +116,4 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None): return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, outputs=outputs) + T=T, outputs=outputs, Js=Js) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 271a204..0c25801 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -16,7 +16,7 @@ ''' -def get_H_U(blocks, unknowns, targets, T, ss=None): +def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. Parameters @@ -27,6 +27,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None): T : int, truncation horizon (if asymptotic, truncation horizon for backward iteration in HetBlocks) ss : [optional] dict, steady state required if blocks contains any non-jacdicts + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians Returns ------- @@ -38,7 +39,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None): """ # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T) + curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, Js) # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) @@ -47,7 +48,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None): return H_U_unpacked[targets, unknowns].pack(T) -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_U=None, H_U_factored=None): +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_U=None, H_U_factored=None, Js=None): """Get a single general equilibrium impulse response. Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed @@ -62,6 +63,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ T : [optional] int, truncation horizon ss : [optional] dict, steady state required if blocks contains non-jacdicts outputs : [optional] list of str, variables we want impulse responses for + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) @@ -75,7 +77,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ T = len(x) break - curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T) + curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, Js) # step 1: if not provided, do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) if H_U is None and H_U_factored is None: @@ -110,7 +112,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None): +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None, Js=None): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -130,6 +132,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_ outputs : [optional] list of str, variables we want impulse responses for H_U : [optional] array, precomputed Jacobian mapping unknowns to targets H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians Returns ------- @@ -137,7 +140,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_ """ # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T) + curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, Js) # step 2: do (matrix) forward accumulation to get # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) @@ -163,7 +166,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_ return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def curlyJ_sorted(blocks, inputs, ss=None, T=None): +def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): """ Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs and with respect to outputs of other blocks @@ -174,6 +177,7 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None): inputs : list, input names we need to differentiate with respect to ss : [optional] dict, steady state, needed if blocks includes blocks themselves T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians Returns ------- @@ -196,13 +200,8 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None): shocks = set(inputs) | required for num in topsorted: block = blocks[num] - - if hasattr(block, 'jacobian'): - jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T}.items() - if k in misc.input_kwarg_list(block.jacobian)}) - else: - # doesn't have 'jac', must be nested dict that is jac directly - jac = block + jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() + if k in misc.input_kwarg_list(block.jacobian)}) # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) # then don't add it to the list of curlyJs to be returned diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 44f56d9..d82062e 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -7,7 +7,7 @@ from .jacobian.support import pack_vectors, unpack_vectors -def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factored=None, monotonic=False, +def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, H_U=None, H_U_factored=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. @@ -20,6 +20,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factore exogenous : dict, all shocked Z go here, must all have same length T unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians H_U : [optional] array (nU*nU), Jacobian of targets with respect to unknowns H_U_factored : [optional] tuple, LU decomposition of H_U, save time by supplying this from utils.misc.factor() monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k @@ -53,7 +54,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, H_U=None, H_U_factore if H_U_factored is None: if H_U is None: # not even H_U is supplied, get it (costly if there are HetBlocks) - H_U = get_H_U(block_list, unknowns, targets, T, ss) + H_U = get_H_U(block_list, unknowns, targets, T, ss, Js) H_U_factored = misc.factor(H_U) # do a topological sort once to avoid some redundancy diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 393a76f..ce5545e 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -86,31 +86,6 @@ def factored_solve(Z, y): return scipy.linalg.lu_solve(Z, y) -# TODO: are these still necessary? -# functions for handling saved Jacobians: extract keys from dicts or key pairs -# from nested dicts, and take subarrays with 'shape' of the values -def extract_dict(savedA, keys, shape): - return {k: take_subarray(savedA[k], shape) for k in keys} - - -def extract_nested_dict(savedA, keys1, keys2, shape): - return {k1: {k2: take_subarray(savedA[k1][k2], shape) for k2 in keys2} for k1 in keys1} - - -def take_subarray(A, shape): - # verify leading dimensions of A are >= shape - if not all(m <= n for m, n in zip(shape, A.shape)): - raise ValueError(f'Saved has dimensions {A.shape}, want larger {shape} subarray') - - # take subarray along those dimensions: A[:shape, ...] - return A[tuple(slice(None, x, None) for x in shape) + (Ellipsis,)] - - -def uncapitalize(s): - # Similar to s.lower() but only makes the first character lower-case - return s[0].lower() + s[1:] - - # The below functions are used in steady_state def unprime(s): """Given a variable's name as a `str`, check if the variable is a prime, i.e. has "_p" at the end. diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index caf53b1..ddb723f 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -49,18 +49,14 @@ def test_ks_td(krusell_smith_dag): def test_hank_td(one_asset_hank_dag): hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag - # TODO: precompute Jacobian and use later T = 30 - G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + H_U = get_H_U(hank_model.blocks, unknowns, targets, T, ss) + G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, H_U=H_U) rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) - H_U = get_H_U(hank_model.blocks, unknowns, targets, T, ss) - H_U_factored = utils.misc.factor(H_U) - - td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, - H_U_factored=H_U_factored) + td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, H_U=H_U) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -72,13 +68,15 @@ def test_two_asset_td(two_asset_hank_dag): two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag T = 30 - # TODO: precompute Jacobian and use later - G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + H_U = get_H_U(two_asset_model.blocks, unknowns, targets, T, ss) + H_U_factored = utils.misc.factor(H_U) + G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, H_U_factored=H_U_factored) for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: drstar = shock_size * -0.0025 * 0.6 ** np.arange(T) - td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets) + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, + H_U_factored=H_U_factored) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_lin = 100 * G['Y']['rstar'] @ drstar @@ -98,19 +96,23 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] T = 30 - # TODO: precompute Jacobian and use later - G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) - G_simple = two_asset_model_simple.solve_jacobian(ss, exogenous, unknowns_simple, targets_simple, T=T) + household = two_asset_model._blocks_unsorted[0] + J_ha = household.jacobian(ss=ss, T=T, exogenous=['N', 'r', 'ra', 'rb', 'tax', 'w']) + G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) + G_simple = two_asset_model_simple.solve_jacobian(ss, exogenous, unknowns_simple, targets_simple, T=T, + Js={'household': J_ha}) drstar = -0.0025 * 0.6 ** np.arange(T) dY = 100 * G['Y']['rstar'] @ drstar - td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets) + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, + Js={'household': J_ha}) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_simple = 100 * G_simple['Y']['rstar'] @ drstar td_nonlin_simple = two_asset_model_simple.solve_impulse_nonlinear(ss, {"rstar": drstar}, - unknowns_simple, targets_simple) + unknowns_simple, targets_simple, + Js={'household': J_ha}) dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) From f1833ba68dd5a80d7e93f5e0650d45e7c99ce17f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Sat, 17 Apr 2021 14:44:55 -0500 Subject: [PATCH 110/288] give informative warning is supplied J did not pass inspection --- sequence_jacobian/blocks/het_block.py | 14 +++-------- sequence_jacobian/blocks/simple_block.py | 11 +------- sequence_jacobian/utilities/misc.py | 32 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index e79a207..e41be7c 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -7,6 +7,7 @@ from .. import utilities as utils from ..jacobian.classes import JacobianDict from ..devtools.deprecate import rename_output_list_to_outputs +from ..utilities.misc import verify_saved_jacobian def het(exogenous, policy, backward, backward_init=None): @@ -397,17 +398,10 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] - # if we supply Jacobians, use them if possible + # if we supply Jacobians, use them if possible, warn if they cannot be used if Js is not None: - # are these the Jacobians you're looking for? - if self.name in Js.keys() and isinstance(Js[self.name], JacobianDict): - J = Js[self.name] - # do they have all the inputs and outputs you need? - outputs_cap = [o.capitalize() for o in outputs] - if set(outputs_cap).issubset(set(J.outputs)) and set(relevant_shocks).issubset(set(J.inputs)): - # do they have the right length? - if T == J[J.outputs[0]][J.inputs[0]].shape[-1]: - return J + if verify_saved_jacobian(self.name, Js, outputs, relevant_shocks, T): + return Js[self.name] # step 0: preliminary processing of steady state (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 6c89fd1..bc324d3 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -119,7 +119,7 @@ def jacobian(self, ss, exogenous=None, T=None, Js=None): exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs Js : [optional] dict of {str: JacobianDict}} - supply saved Jacobians + supply saved Jacobians, unnecessary for simple blocks Returns ------- @@ -134,15 +134,6 @@ def jacobian(self, ss, exogenous=None, T=None, Js=None): relevant_shocks = [i for i in self.inputs if i in exogenous] - # if we supply Jacobians, use them if possible - if Js is not None: - # are these the Jacobians you're looking for? - if self.name in Js.keys() and isinstance(Js[self.name], JacobianDict): - J = Js[self.name] - # do they have all the inputs you need? - if set(relevant_shocks).issubset(set(J.inputs)): - return J - # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict if not relevant_shocks: diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index ce5545e..fe6c056 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -4,6 +4,8 @@ import scipy.linalg import re import inspect +import warnings +from ..jacobian.classes import JacobianDict def make_tuple(x): @@ -128,3 +130,33 @@ def smart_zeros(n): return np.zeros(n) else: return 0. + + +def verify_saved_jacobian(block_name, Js, outputs, inputs, T): + """Verify that pre-computed Jacobian has all the right outputs, inputs, and length.""" + if block_name not in Js.keys(): + # don't throw warning, this will happen often for simple blocks + return False + J = Js[block_name] + + if not isinstance(J, JacobianDict): + warnings.warn(f'Js[{block_name}] is not a JacobianDict.') + return False + + outputs_cap = [o.capitalize() for o in outputs] + if not set(outputs_cap).issubset(set(J.outputs)): + missing = set(outputs_cap).difference(set(J.outputs)) + warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') + return False + + if not set(inputs).issubset(set(J.inputs)): + missing = set(inputs).difference(set(J.inputs)) + warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') + return False + + Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] + if T != Tsaved: + warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') + return False + + return True From de729ea0aeacf00248fbada229093e8f1e0ccc3d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 19 Apr 2021 14:57:54 -0500 Subject: [PATCH 111/288] Removed vacuous `Js=` kwarg from simple blocks. --- sequence_jacobian/blocks/simple_block.py | 8 +++----- sequence_jacobian/jacobian/drivers.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index bc324d3..f4b05af 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -103,10 +103,10 @@ def impulse_nonlinear(self, ss, exogenous): return ImpulseDict(self._output_in_td_format(**input_args), ss) - def impulse_linear(self, ss, exogenous, T=None, Js=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous), ss) + def impulse_linear(self, ss, exogenous, T=None): + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=None, Js=None): + def jacobian(self, ss, exogenous=None, T=None): """Assemble nested dict of Jacobians Parameters @@ -118,8 +118,6 @@ def jacobian(self, ss, exogenous=None, T=None, Js=None): if omitted, more efficient SimpleSparse objects returned exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs - Js : [optional] dict of {str: JacobianDict}} - supply saved Jacobians, unnecessary for simple blocks Returns ------- diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 0c25801..6956221 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -200,8 +200,8 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): shocks = set(inputs) | required for num in topsorted: block = blocks[num] - jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() - if k in misc.input_kwarg_list(block.jacobian)}) + jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T, "Js": Js}.items() + if k in misc.input_kwarg_list(block.jacobian)}) # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) # then don't add it to the list of curlyJs to be returned From f2a785bd266c5f9a7c4b1cccb6a5d52974b4a78b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 19 Apr 2021 16:48:55 -0500 Subject: [PATCH 112/288] ALl blocks accept and use `Js=`. Simple blocks keep ZeroMatrices so supplied Js can be screened for completeness. --- sequence_jacobian/blocks/combined_block.py | 4 ++-- sequence_jacobian/blocks/het_block.py | 3 ++- sequence_jacobian/blocks/simple_block.py | 28 ++++++++++++---------- sequence_jacobian/blocks/solved_block.py | 5 ++-- sequence_jacobian/jacobian/drivers.py | 4 ++-- sequence_jacobian/primitives.py | 12 ++++++---- sequence_jacobian/utilities/misc.py | 15 ++++++------ 7 files changed, 39 insertions(+), 32 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index eaebd52..46fb395 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -80,7 +80,7 @@ def impulse_nonlinear(self, ss, exogenous, **kwargs): return ImpulseDict(irf_nonlin_partial_eq, ss).levels() - def impulse_linear(self, ss, exogenous, T=None): + def impulse_linear(self, ss, exogenous, T=None, Js=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from a steady_state, `ss`""" irf_lin_partial_eq = deepcopy(exogenous) @@ -88,7 +88,7 @@ def impulse_linear(self, ss, exogenous, T=None): input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T)}) + irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T, Js=Js)}) return ImpulseDict(irf_lin_partial_eq, ss) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index e41be7c..2e76e85 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -400,7 +400,8 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js # if we supply Jacobians, use them if possible, warn if they cannot be used if Js is not None: - if verify_saved_jacobian(self.name, Js, outputs, relevant_shocks, T): + outputs_cap = [o.capitalize() for o in outputs] + if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): return Js[self.name] # step 0: preliminary processing of steady state diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index f4b05af..29b0c1d 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -4,7 +4,7 @@ from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .support.impulse import ImpulseDict from ..primitives import Block -from ..jacobian.classes import JacobianDict, SimpleSparse +from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix from ..utilities import misc '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -103,10 +103,10 @@ def impulse_nonlinear(self, ss, exogenous): return ImpulseDict(self._output_in_td_format(**input_args), ss) - def impulse_linear(self, ss, exogenous, T=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T).apply(exogenous), ss) + def impulse_linear(self, ss, exogenous, T=None, Js=None): + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous), ss) - def jacobian(self, ss, exogenous=None, T=None): + def jacobian(self, ss, exogenous=None, T=None, Js=None): """Assemble nested dict of Jacobians Parameters @@ -118,6 +118,8 @@ def jacobian(self, ss, exogenous=None, T=None): if omitted, more efficient SimpleSparse objects returned exogenous : list of str, optional names of input variables to differentiate wrt; if omitted, assume all inputs + Js : [optional] dict of {str: JacobianDict}} + supply saved Jacobians, unnecessary for simple blocks Returns ------- @@ -132,6 +134,11 @@ def jacobian(self, ss, exogenous=None, T=None): relevant_shocks = [i for i in self.inputs if i in exogenous] + # if we supply Jacobians, use them if possible, warn if they cannot be used + if Js is not None: + if misc.verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): + return Js[self.name] + # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks # are an input into the block), then return an empty dict if not relevant_shocks: @@ -149,19 +156,14 @@ def jacobian(self, ss, exogenous=None, T=None): J = {o: {} for o in self.output_list} for o in self.output_list: for i in relevant_shocks: - # Do not write an entry into J if shock `i` did not affect output `o` + # Keep zeros, so we can inspect supplied Jacobians for completeness if not invertedJ[i][o] or invertedJ[i][o].iszero: - continue + J[o][i] = ZeroMatrix() else: if T is not None: - J[o][i] = invertedJ[i][o].nonzero().matrix(T) + J[o][i] = invertedJ[i][o].matrix(T) else: - J[o][i] = invertedJ[i][o].nonzero() - - # If output `o` is entirely unaffected by all of the shocks passed in, then - # remove the empty Jacobian corresponding to `o` from J - if not J[o]: - del J[o] + J[o][i] = invertedJ[i][o] return JacobianDict(J, name=self.name) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 2f37d54..44c2df3 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -95,10 +95,9 @@ def steady_state(self, calibration, helper_blocks=None, consistency_check=True, return super().solve_steady_state(calibration, self.unknowns, self.targets, solver=self.solver, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) - def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, - returnindividual=False, verbose=False): + def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): return super().solve_impulse_nonlinear(ss, exogenous=exogenous, - unknowns=list(self.unknowns.keys()), + unknowns=list(self.unknowns.keys()), Js=Js, targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 6956221..0c25801 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -200,8 +200,8 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): shocks = set(inputs) | required for num in topsorted: block = blocks[num] - jac = block.jacobian(ss, exogenous=list(shocks), **{k: v for k, v in {"T": T, "Js": Js}.items() - if k in misc.input_kwarg_list(block.jacobian)}) + jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() + if k in misc.input_kwarg_list(block.jacobian)}) # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) # then don't add it to the list of curlyJs to be returned diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index f4e05bf..f911589 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -99,6 +99,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], + Js: Optional[Dict[str, JacobianDict]] = None, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous @@ -106,27 +107,30 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], blocks = self.blocks if hasattr(self, "blocks") else [self] irf_nonlin_gen_eq = td_solve(blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, - unknowns=unknowns, targets=targets, **kwargs) + unknowns=unknowns, targets=targets, Js=Js, **kwargs) return ImpulseDict(irf_nonlin_gen_eq, ss) def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], T: Optional[int] = None, + Js: Optional[Dict[str, JacobianDict]] = None, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] - irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) + irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) return ImpulseDict(irf_lin_gen_eq, ss) def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str], unknowns: List[str], targets: List[str], - T: Optional[int] = None, **kwargs) -> JacobianDict: + T: Optional[int] = None, + Js: Optional[Dict[str, JacobianDict]] = None, + **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] - return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, **kwargs) + return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index fe6c056..ba8f4d0 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -143,9 +143,8 @@ def verify_saved_jacobian(block_name, Js, outputs, inputs, T): warnings.warn(f'Js[{block_name}] is not a JacobianDict.') return False - outputs_cap = [o.capitalize() for o in outputs] - if not set(outputs_cap).issubset(set(J.outputs)): - missing = set(outputs_cap).difference(set(J.outputs)) + if not set(outputs).issubset(set(J.outputs)): + missing = set(outputs).difference(set(J.outputs)) warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') return False @@ -154,9 +153,11 @@ def verify_saved_jacobian(block_name, Js, outputs, inputs, T): warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') return False - Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] - if T != Tsaved: - warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') - return False + # Jacobian of simple blocks may have a sparse representation + if T is not None: + Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] + if T != Tsaved: + warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') + return False return True From 46a53dd32f2077a79e49f06946b957080dcb7e52 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 19 Apr 2021 17:33:27 -0500 Subject: [PATCH 113/288] Removed `H_U=` and `H_U_factored=` kwargs. --- sequence_jacobian/jacobian/drivers.py | 35 ++++++------------------ sequence_jacobian/nonlinear.py | 20 +++++++------- tests/base/test_transitional_dynamics.py | 15 +++++----- 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py index 0c25801..830554b 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/sequence_jacobian/jacobian/drivers.py @@ -48,7 +48,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): return H_U_unpacked[targets, unknowns].pack(T) -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_U=None, H_U_factored=None, Js=None): +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js=None): """Get a single general equilibrium impulse response. Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed @@ -64,8 +64,6 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ ss : [optional] dict, steady state required if blocks contains non-jacdicts outputs : [optional] list of str, variables we want impulse responses for Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - H_U : [optional] array, precomputed Jacobian mapping unknowns to targets - H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) Returns ------- @@ -79,9 +77,8 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, Js) - # step 1: if not provided, do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) - if H_U is None and H_U_factored is None: - H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) + # step 1: do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) + H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) # step 2: do (vector) forward accumulation to get J^(o, curlyZ)dZ for all o in # 'alloutputs', the combination of outputs (if specified) and targets @@ -92,16 +89,9 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ J_curlyZ_dZ = forward_accumulate(curlyJs, dZ, alloutputs, required) # step 3: solve H_UdU = -H_ZdZ for dU - if H_U is None and H_U_factored is None: - H_U = H_U_unpacked[targets, unknowns].pack(T) - + H_U = H_U_unpacked[targets, unknowns].pack(T) H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) - - if H_U_factored is None: - dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) - else: - dU_packed = -misc.factored_solve(H_U_factored, H_ZdZ_packed) - + dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) dU = unpack_vectors(dU_packed, unknowns, T) # step 4: do (vector) forward accumulation to get J^(o, curlyU)dU @@ -112,7 +102,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, H_ return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_U=None, H_U_factored=None, Js=None): +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js=None): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -130,8 +120,6 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_ T : [optional] int, truncation horizon ss : [optional] dict, steady state required if blocks contains non-jacdicts outputs : [optional] list of str, variables we want impulse responses for - H_U : [optional] array, precomputed Jacobian mapping unknowns to targets - H_U_factored : [optional] tuple of arrays, precomputed LU factorization utils.misc.factor(H_U) Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians Returns @@ -144,19 +132,14 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, H_ # step 2: do (matrix) forward accumulation to get # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) - if H_U is None and H_U_factored is None: - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) + J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) # step 3: solve for G^U, unpack - if H_U is None and H_U_factored is None: - H_U = J_curlyH_U[targets, unknowns].pack(T) + H_U = J_curlyH_U[targets, unknowns].pack(T) H_Z = J_curlyH_Z[targets, exogenous].pack(T) - if H_U_factored is None: - G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) - else: - G_U = JacobianDict.unpack(-misc.factored_solve(H_U_factored, H_Z), unknowns, exogenous, T) + G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) # step 4: forward accumulation to get all outputs starting with G_U # by default, don't calculate targets! diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index d82062e..954957a 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -7,7 +7,7 @@ from .jacobian.support import pack_vectors, unpack_vectors -def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, H_U=None, H_U_factored=None, monotonic=False, +def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. @@ -21,8 +21,6 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, H_U=None, H_ unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) targets : list, targets of SHADE DAG, the 'H' in H(U, Z) Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - H_U : [optional] array (nU*nU), Jacobian of targets with respect to unknowns - H_U_factored : [optional] tuple, LU decomposition of H_U, save time by supplying this from utils.misc.factor() monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k (allows more efficient interpolation) returnindividual: [optional] bool, flag to return individual outcomes from HetBlock.td @@ -45,16 +43,18 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, H_U=None, H_ for v in exogenous.values(): T = v.shape[0] break - + # initialize guess for unknowns to steady state length T unknown_paths = {k: np.full(T, ss[k]) for k in unknowns} Uvec = pack_vectors(unknown_paths, unknowns, T) - # obtain H_U_factored if we don't have it already - if H_U_factored is None: - if H_U is None: - # not even H_U is supplied, get it (costly if there are HetBlocks) - H_U = get_H_U(block_list, unknowns, targets, T, ss, Js) + # Obtain H_U_factored if we don't have it already + if isinstance(Js, np.ndarray): + # if Js is a matrix, assume it's a packed H_U + H_U_factored = misc.factor(Js) + else: + # not even H_U is supplied, get it (costly if there are HetBlocks) + H_U = get_H_U(block_list, unknowns, targets, T, ss, Js) H_U_factored = misc.factor(H_U) # do a topological sort once to avoid some redundancy @@ -79,7 +79,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, H_U=None, H_ unknown_paths = unpack_vectors(Uvec, unknowns, T) else: raise ValueError(f'No convergence after {maxit} backward iterations!') - + return results diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index ddb723f..a34fc2f 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -50,13 +50,14 @@ def test_hank_td(one_asset_hank_dag): hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag T = 30 - H_U = get_H_U(hank_model.blocks, unknowns, targets, T, ss) - G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, H_U=H_U) + household = hank_model._blocks_unsorted[0] + J_ha = household.jacobian(ss=ss, T=T, exogenous=['Div', 'Tax', 'r', 'w']) + G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) - td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, H_U=H_U) + td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, Js={'household': J_ha}) dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -68,15 +69,15 @@ def test_two_asset_td(two_asset_hank_dag): two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag T = 30 - H_U = get_H_U(two_asset_model.blocks, unknowns, targets, T, ss) - H_U_factored = utils.misc.factor(H_U) - G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, H_U_factored=H_U_factored) + household = two_asset_model._blocks_unsorted[0] + J_ha = household.jacobian(ss=ss, T=T, exogenous=['N', 'r', 'ra', 'rb', 'tax', 'w']) + G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: drstar = shock_size * -0.0025 * 0.6 ** np.arange(T) td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, - H_U_factored=H_U_factored) + Js={'household': J_ha}) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_lin = 100 * G['Y']['rstar'] @ drstar From c011a19b8c5da696de0270348b939c3fa2a342eb Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 23 Apr 2021 14:32:21 -0500 Subject: [PATCH 114/288] Removed `Js=H_U` functionality from nonlinear.td_solve. --- sequence_jacobian/nonlinear.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 954957a..2d9bc97 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -48,14 +48,9 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=Fa unknown_paths = {k: np.full(T, ss[k]) for k in unknowns} Uvec = pack_vectors(unknown_paths, unknowns, T) - # Obtain H_U_factored if we don't have it already - if isinstance(Js, np.ndarray): - # if Js is a matrix, assume it's a packed H_U - H_U_factored = misc.factor(Js) - else: - # not even H_U is supplied, get it (costly if there are HetBlocks) - H_U = get_H_U(block_list, unknowns, targets, T, ss, Js) - H_U_factored = misc.factor(H_U) + # obtain Jacobian of targets wrt to unknowns + H_U = get_H_U(block_list, unknowns, targets, T, ss, Js) + H_U_factored = misc.factor(H_U) # do a topological sort once to avoid some redundancy sort = graph.block_sort(block_list) From c15c3a688ea96c8d6cc5018bb976077a80fc7f38 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 23 Apr 2021 14:44:19 -0500 Subject: [PATCH 115/288] Start with SteadyStateDict template @mrognlie provided --- sequence_jacobian/steady_state/classes.py | 46 ++++++----------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index db95c47..8e52817 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -5,40 +5,18 @@ class SteadyStateDict: - def __init__(self, raw_dict, blocks=None): - # TODO: Will need to re-think flat storage of data if/when we move to implementing remapping - self.data = deepcopy(raw_dict) + def __init__(self, toplevel, internal): + self.toplevel = toplevel + self.internal = internal - if blocks is not None: - self.block_map = {b.name: b.outputs for b in blocks} - self.block_names = list(self.block_map.keys()) - - # Record the values in raw_dict not output by any of the blocks as part of the `calibration` - self.block_map["calibration"] = set(raw_dict.keys()) - set().union(*self.block_map.values()) - else: - self.block_map = {"calibration": set(raw_dict.keys())} - self.block_names = ["calibration"] - - def __repr__(self, raw=False): - if set(self.block_names) == {"calibration"} or raw: - return self.data.__repr__() + def __getitem__(self, k): + if isinstance(k, str): + return self.toplevel[k] else: - return f"<{type(self).__name__} blocks={self.block_names}" - - def __getitem__(self, key): - if key in self.block_names: - return {k: v for k, v in self.data.items() if k in self.block_map[key]} - else: - return self.data[key] - - def __setitem__(self, key, value): - if key not in self.data: - block_name_to_assign = "calibration" - self.block_map[block_name_to_assign] = self.block_map[block_name_to_assign] | {key} - self.data[key] = value - - def aggregates(self): - return {k: v for k, v in self.data.items() if np.isscalar(v)} + try: + return {ki: self.toplevel[ki] for ki in k} + except TypeError: + raise TypeError(f'Key {k} needs to be a string or an iterable (list, set, etc) of strings') - def idiosyncratic_variables(self): - return {k: v for k, v in self.data.items() if not np.isscalar(v)} + def __setitem__(self, k, v): + self.toplevel[k] = v From 508d22660234896ec10bf46fb793ae3a676019a1 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 23 Apr 2021 15:17:03 -0500 Subject: [PATCH 116/288] Add additional structure to SteadyStateDict and make compatible with rbc steady state --- sequence_jacobian/blocks/simple_block.py | 18 +++++------ sequence_jacobian/steady_state/classes.py | 39 +++++++++++++++++++++-- sequence_jacobian/steady_state/drivers.py | 12 ++++--- 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index f4b05af..7380186 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -1,9 +1,13 @@ +"""Class definition of a simple block""" + import warnings import numpy as np +from copy import deepcopy from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .support.impulse import ImpulseDict from ..primitives import Block +from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict, SimpleSparse from ..utilities import misc @@ -58,18 +62,12 @@ def jac(self, ss, T=None, shock_list=None): DeprecationWarning) return self.jacobian(ss, exogenous=shock_list, T=T) - def _output_in_ss_format(self, **kwargs): - """Returns output of the method ss as either a tuple of numeric primitives (scalars/vectors) or a single - numeric primitive, as opposed to Ignore/IgnoreVector objects""" - if len(self.output_list) > 1: - return dict(zip(self.output_list, [misc.numeric_primitive(o) for o in self.f(**kwargs)])) - else: - return dict(zip(self.output_list, [misc.numeric_primitive(self.f(**kwargs))])) - def steady_state(self, calibration): - input_args = {k: ignore(v) for k, v in calibration.items()} - return self._output_in_ss_format(**input_args) + input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} + output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [misc.numeric_primitive(self.f(**input_args))] + return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) + # TODO: Remove this so everything is just done within impulse_nonlinear def _output_in_td_format(self, **kwargs_new): """Returns output of the method td as a dict mapping output names to numeric primitives (scalars/vectors) or a single numeric primitive of output values, as opposed to Ignore/IgnoreVector/Displace objects. diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index 8e52817..8b702c6 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -3,11 +3,26 @@ from copy import deepcopy import numpy as np +from ..utilities.misc import dict_diff + class SteadyStateDict: - def __init__(self, toplevel, internal): - self.toplevel = toplevel - self.internal = internal + def __init__(self, data, internal=None): + if isinstance(data, SteadyStateDict): + self.toplevel = deepcopy(data.toplevel) + self.internal = deepcopy(data.internal) + else: + self.toplevel = data + self.internal = internal if internal is not None else {} + + def __repr__(self): + if self.internal: + return f"{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}" + else: + return f"{type(self).__name__}: {list(self.toplevel.keys())}" + + def __iter__(self): + return iter(self.toplevel) def __getitem__(self, k): if isinstance(k, str): @@ -20,3 +35,21 @@ def __getitem__(self, k): def __setitem__(self, k, v): self.toplevel[k] = v + + def keys(self): + return self.toplevel.keys() + + def values(self): + return self.toplevel.values() + + def update(self, new_data): + if isinstance(new_data, SteadyStateDict): + self.toplevel.update(new_data.toplevel) + self.internal.update(new_data.internal) + else: + # TODO: This is assuming new_data only contains aggregates. Upgrade in later commit to handle the case of + # vector-valued variables/collection into internal namespaces + self.toplevel.update(new_data) + + def difference(self, data_to_remove): + return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 4e3375f..1ea83b3 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -8,6 +8,7 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs, find_excludable_helper_blocks +from .classes import SteadyStateDict from ..utilities import solvers, graph, misc @@ -76,7 +77,7 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, targets = {t: 0. for t in targets} if isinstance(targets, list) else targets helper_targets = {t: targets[t] for t in targets if t in helper_targets} - ss_values = deepcopy(calibration) + ss_values = SteadyStateDict(calibration) ss_values.update(helper_targets) helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) @@ -104,17 +105,17 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): # Want to see hetoutputs elif hasattr(blocks_all[i], 'hetoutput') and blocks_all[i].hetoutput is not None: outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) - ss_values.update(misc.dict_diff(outputs, helper_outputs)) + ss_values.update(outputs.difference(helper_outputs)) else: outputs = eval_block_ss(blocks_all[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: - helper_outputs.update(outputs) + helper_outputs.update(outputs.toplevel) ss_values.update(outputs) else: # Don't overwrite entries in ss_values corresponding to what has already # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(misc.dict_diff(outputs, helper_outputs)) + ss_values.update(outputs.difference(helper_outputs)) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" @@ -150,7 +151,8 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): def eval_block_ss(block, calibration, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" - return block.steady_state({k: v for k, v in calibration.items() if k in block.inputs}, + input_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel + return block.steady_state({k: v for k, v in input_dict.items() if k in block.inputs}, **{k: v for k, v in kwargs.items() if k in misc.input_kwarg_list(block.steady_state)}) From 7c951b14049a2afaa2ee76d76276bd58f446962f Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 23 Apr 2021 16:29:20 -0500 Subject: [PATCH 117/288] Support the construction of internal namespaces for SteadyStateDict when provided with the block and implement SteadyStateDict as the return type for HetBlocks --- sequence_jacobian/blocks/het_block.py | 3 +- sequence_jacobian/steady_state/classes.py | 37 ++++++++++++++++++----- sequence_jacobian/utilities/misc.py | 8 +++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index b477ae2..728b37b 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -5,6 +5,7 @@ from .support.impulse import ImpulseDict from ..primitives import Block from .. import utilities as utils +from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict from ..devtools.deprecate import rename_output_list_to_outputs @@ -250,7 +251,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, # clear any previously saved Jacobian info for safety, since we're computing new SS self.clear_saved() - return ss + return SteadyStateDict(ss, internal=self) def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index 8b702c6..3a39d88 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -1,25 +1,48 @@ """Various classes to support the computation of steady states""" from copy import deepcopy -import numpy as np -from ..utilities.misc import dict_diff +from ..utilities.misc import dict_diff, smart_set + + +def construct_internal_namespace(data, block): + # Only supporting internal namespaces for HetBlocks currently + if hasattr(block, "back_step_fun"): + return {block.name: {k: v for k, v in deepcopy(data).items() if k in + smart_set(block.back_step_outputs) | smart_set(block.exogenous) | {"D"} | + smart_set(block.hetinput_outputs) | smart_set(block.hetoutput_outputs)}} + else: + return {} class SteadyStateDict: def __init__(self, data, internal=None): if isinstance(data, SteadyStateDict): - self.toplevel = deepcopy(data.toplevel) self.internal = deepcopy(data.internal) + self.toplevel = deepcopy(data.toplevel) else: - self.toplevel = data - self.internal = internal if internal is not None else {} + if internal is not None: + # Either we can construct the internal namespace for you (if it's a Block) otherwise, + # you can provide the nested dict representing the internal namespace directly + if hasattr(internal, "inputs") and hasattr(internal, "outputs"): + self.internal = construct_internal_namespace(data, internal) + else: + self.internal = internal + else: + self.internal = {} + if self.internal: + toplevel = deepcopy(data) + for internal_dict in self.internal.values(): + toplevel = dict_diff(toplevel, internal_dict) + self.toplevel = toplevel + else: + self.toplevel = deepcopy(data) def __repr__(self): if self.internal: - return f"{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}" + return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" else: - return f"{type(self).__name__}: {list(self.toplevel.keys())}" + return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" def __iter__(self): return iter(self.toplevel) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index 09cef95..4b66ab2 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -138,6 +138,14 @@ def dict_diff(d1, d2): return o_dict +def smart_set(data): + # We want set to construct a single-element set for strings, i.e. ignoring the .iter method of strings + if isinstance(data, str): + return {data} + else: + return set(data) + + def smart_zip(keys, values): """For handling the case where keys and values may be scalars""" if isinstance(values, float): From 3dcc948c133488edda80842bf87b943c735f4edb Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 23 Apr 2021 16:43:01 -0500 Subject: [PATCH 118/288] Update hank and two_asset legacy steady state code to be compatible with new SteadyStateDict functionality --- sequence_jacobian/models/hank.py | 14 ++------------ sequence_jacobian/models/two_asset.py | 7 ++++--- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/sequence_jacobian/models/hank.py b/sequence_jacobian/models/hank.py index 02c7724..38cf0ee 100644 --- a/sequence_jacobian/models/hank.py +++ b/sequence_jacobian/models/hank.py @@ -191,7 +191,6 @@ def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1 T = transfers(pi_e, Div, Tax, e_grid) # initialize guess for policy function iteration - fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] Va = (1 + r) * (0.1 * coh) ** (-1 / eis) @@ -199,21 +198,18 @@ def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1 def res(x): beta_loc, vphi_loc = x # precompute constrained c and n which don't depend on Va - c_const_loc, n_const_loc = solve_cn(w * e_grid[:, np.newaxis], fininc, eis, frisch, vphi_loc, Va) if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001: raise ValueError('Clearly invalid inputs') out = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta_loc, - eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc, - c_const=c_const_loc, n_const=n_const_loc) + eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc) return np.array([out['A'] - B, out['N_e'] - 1]) # solve for beta, vphi (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) # extra evaluation for reporting - c_const, n_const = solve_cn(w * e_grid[:, np.newaxis], fininc, eis, frisch, vphi, Va) ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis, - Div=Div, Tax=Tax, frisch=frisch, vphi=vphi, c_const=c_const, n_const=n_const) + Div=Div, Tax=Tax, frisch=frisch, vphi=vphi) # check Walras's law goods_mkt = 1 - ss['C'] @@ -224,10 +220,4 @@ def res(x): 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) - # since we don't use c_const, n_const (an optimization of pre-calculating the constrained consumption and labor - # supply) in the general steady_state function, delete it from the ss dict so the test won't - # check for those variables - del ss["c_const"] - del ss["n_const"] - return ss diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 9cd64e5..91ecc98 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -385,12 +385,13 @@ def res(x): pshare = p / (tot_wealth - Bh) # calculate aggregate adjustment cost and check Walras's law - chi = get_Psi_and_deriv(ss['a'], a_grid, r, chi0, chi1, chi2)[0] - Chi = np.vdot(ss['D'], chi) + chi = get_Psi_and_deriv(ss.internal["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] + Chi = np.vdot(ss.internal["household"]['D'], chi) goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 + ss.internal["household"].update({"chi": chi}) ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, - 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'chi': chi, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, + 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'z_grid': z_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, From 375e62a73974229e8bbc9437becee6287b800397 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:15:26 -0500 Subject: [PATCH 119/288] Add .items method to SteadyStateDict --- sequence_jacobian/steady_state/classes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index 3a39d88..6544c37 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -65,6 +65,9 @@ def keys(self): def values(self): return self.toplevel.values() + def items(self): + return self.toplevel.items() + def update(self, new_data): if isinstance(new_data, SteadyStateDict): self.toplevel.update(new_data.toplevel) From f2359b1a7d2de162a0813f79adf5e25182db4582 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:15:57 -0500 Subject: [PATCH 120/288] Make jacobian/impulse_* methods for SimpleBlock and HetBlock compatible with SteadyStateDict --- sequence_jacobian/blocks/het_block.py | 55 ++++++++++++--------- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/blocks/support/impulse.py | 7 ++- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 728b37b..2dd11a8 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -291,7 +291,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal # copy from ss info Pi_T = ss[self.exogenous].T.copy() - D = ss['D'] + D = ss.internal[self.name]['D'] # construct grids for policy variables either from the steady state grid if the grid is meant to be # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. @@ -310,10 +310,11 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} # backward iteration - backdict = ss.copy() + backdict = dict(ss.items()) + backdict.update(copy.deepcopy(ss.internal[self.name])) for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t,...] for k, v in exogenous.items()}) + backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -425,14 +426,15 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, h= curlyYs, curlyDs = {}, {} for i in relevant_shocks: curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, - ss['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, h, - ss_for_hetinput) + ss.internal[self.name]['D'], Pi.T.copy(), + sspol_i, sspol_pi, sspol_space, T, h, + ss_for_hetinput) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o curlyPs = {} for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1) + curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair @@ -781,7 +783,7 @@ def jac_prelim(self, ss, save=False, use_saved=False): sspol_space = {} for pol in self.policy: # use robust binary-search-based method that only requires grids to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss[pol]) + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) @@ -805,21 +807,30 @@ def make_inputs(self, back_step_inputs_dict): """ input_dict = copy.deepcopy(back_step_inputs_dict) - # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include - # them as inputs for self.back_step_fun - if self.hetinput is not None: - outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] - for k in self.hetinput_inputs if k in input_dict})) - input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) - - # Check if there are entries in indict corresponding to self.inputs_to_be_primed. - # In particular, we are interested in knowing if an initial value - # for the backward iteration variable has been provided. - # If it has not been provided, then use self.backward_init to calculate the initial values. - if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): - initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] - input_dict.update(zip(utils.misc.output_list(self.backward_init), - utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) + # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady + # state computation as well as for Jacobian/impulse evaluation for HetBlocks, + # where in the former the hetinputs and value function have yet to be computed, + # whereas in the latter they have already been computed + # and hence do not need to be recomputed. There may be room to clean this function up a bit. + if isinstance(back_step_inputs_dict, SteadyStateDict): + input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) + input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) + else: + # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include + # them as inputs for self.back_step_fun + if self.hetinput is not None: + outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] + for k in self.hetinput_inputs if k in input_dict})) + input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) + + # Check if there are entries in indict corresponding to self.inputs_to_be_primed. + # In particular, we are interested in knowing if an initial value + # for the backward iteration variable has been provided. + # If it has not been provided, then use self.backward_init to calculate the initial values. + if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): + initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] + input_dict.update(zip(utils.misc.output_list(self.backward_init), + utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) for i_p in self.inputs_to_be_primed: input_dict[i_p + "_p"] = input_dict[i_p] diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 7380186..45dc0e2 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -93,7 +93,7 @@ def impulse_nonlinear(self, ss, exogenous): for k, v in exogenous.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') - input_args[k] = Displace(v + ss[k], ss=ss.get(k, None), name=k) + input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) for k in self.input_list: if k not in input_args: diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index 18dd39e..4b75584 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -2,6 +2,8 @@ import numpy as np +from ...steady_state.classes import SteadyStateDict + class ImpulseDict: def __init__(self, impulse, ss): @@ -9,8 +11,9 @@ def __init__(self, impulse, ss): self.impulse = impulse.impulse self.ss = impulse.ss else: - if not isinstance(impulse, dict) or not isinstance(ss, dict): - raise ValueError('ImpulseDicts are initialized with two dicts.') + if not isinstance(impulse, dict) or not isinstance(ss, SteadyStateDict): + raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses' + ' and a `SteadyStateDict` of steady state values.') self.impulse = impulse self.ss = ss From 65a0f34c9229bd617005f330716e02ab8426b386 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:18:00 -0500 Subject: [PATCH 121/288] Alter the contents of legacy code steady state outputs for krusell_smith, hank, and two_asset. Notes below: - For krusell_smith and hank the one different entry in the legacy code was it did not contain Pi, since Pi is constructed in a block separate from household and was not previously included to be explicitly returned by the legacy code. - For two_asset the different entry was z_grid, since now we do not keep hetinput_outputs at the top level. --- sequence_jacobian/models/hank.py | 2 +- sequence_jacobian/models/krusell_smith.py | 5 +++-- sequence_jacobian/models/two_asset.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/models/hank.py b/sequence_jacobian/models/hank.py index 38cf0ee..8c2ce79 100644 --- a/sequence_jacobian/models/hank.py +++ b/sequence_jacobian/models/hank.py @@ -216,7 +216,7 @@ def res(x): assert np.abs(goods_mkt) < 1E-8 # add aggregate variables - ss.update({'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, + ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) diff --git a/sequence_jacobian/models/krusell_smith.py b/sequence_jacobian/models/krusell_smith.py index ecdb0b9..82e4559 100644 --- a/sequence_jacobian/models/krusell_smith.py +++ b/sequence_jacobian/models/krusell_smith.py @@ -118,7 +118,8 @@ def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, # extra evaluation to report variables ss = household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=beta, eis=eis, Va=Va) - ss.update({'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, 'goods_mkt': Y - ss['C'] - delta * K, - 'nA': nA, 'amax': amax, 'sigma': sigma, 'rho': rho, 'nS': nS, 'asset_mkt': ss["A"] - K}) + ss.update({'Pi': Pi, 'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, + 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, + 'rho': rho, 'nS': nS, 'asset_mkt': ss["A"] - K}) return ss diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index 91ecc98..c4d8ef4 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -393,7 +393,7 @@ def res(x): ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, - 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'z_grid': z_grid, 'e_grid': e_grid, + 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, 'goods_mkt': goods_mkt, 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], From 1794c05d1929b364a65622e4459b919c72de68f4 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:18:11 -0500 Subject: [PATCH 122/288] Update tests to be compatible with SteadyStateDict --- tests/base/test_jacobian.py | 7 ++++--- tests/base/test_simple_block.py | 12 +++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 55e795e..e097bb9 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -74,8 +74,9 @@ def test_fake_news_v_actual(one_asset_hank_dag): curlyYs, curlyDs = {}, {} for i in shock_list: curlyYs[i], curlyDs[i] = household.backward_iteration_fakenews(i, output_list, ssin_dict, - ssout_list, ss['D'], Pi.T.copy(), sspol_i, - sspol_pi, sspol_space, T, h, ss_for_hetinput) + ssout_list, ss.internal["household"]['D'], + Pi.T.copy(), sspol_i, sspol_pi, sspol_space, + T, h, ss_for_hetinput) asset_effects = np.sum(curlyDs['r'] * ss['a_grid'], axis=(1, 2)) assert np.linalg.norm(asset_effects - curlyYs["r"]["a"], np.inf) < 2e-15 @@ -83,7 +84,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): # Step 2 of fake news algorithm: (transpose) forward iteration curlyPs = {} for o in output_list: - curlyPs[o] = household.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1) + curlyPs[o] = household.forward_iteration_fakenews(ss.internal["household"][o], Pi, sspol_i, sspol_pi, T-1) persistent_asset = np.array([np.vdot(curlyDs['r'][0, ...], curlyPs['a'][u, ...]) for u in range(30)]) diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index b120d9a..781ecd4 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -1,9 +1,11 @@ """Test SimpleBlock functionality""" -from sequence_jacobian import simple, utilities import numpy as np import pytest +from sequence_jacobian import simple +from sequence_jacobian.steady_state.classes import SteadyStateDict + @simple def F(K, L, Z, alpha): @@ -27,10 +29,10 @@ def taylor(r, pi, phi): return i -@pytest.mark.parametrize("block,ss", [(F, {"K": 1, "L": 1, "Z": 1, "alpha": 0.5}), - (investment, {"Q": 1, "K": 1, "r": 0.05, "N": 1, "mc": 1, "Z": 1, "delta": 0.05, - "epsI": 2, "alpha": 0.5}), - (taylor, {"r": 0.05, "pi": 0.01, "phi": 1.5})]) +@pytest.mark.parametrize("block,ss", [(F, SteadyStateDict({"K": 1, "L": 1, "Z": 1, "alpha": 0.5})), + (investment, SteadyStateDict({"Q": 1, "K": 1, "r": 0.05, "N": 1, "mc": 1, + "Z": 1, "delta": 0.05, "epsI": 2, "alpha": 0.5})), + (taylor, SteadyStateDict({"r": 0.05, "pi": 0.01, "phi": 1.5}))]) def test_block_consistency(block, ss): """Make sure ss, td, and jac methods are all consistent with each other. Requires that all inputs of simple block allow calculating Jacobians""" From 7e17913ec92e646f03be48ec42d4b01faef26b4a Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:32:16 -0500 Subject: [PATCH 123/288] Fix .update method for SteadyStateDict and initialize with it --- sequence_jacobian/steady_state/classes.py | 49 ++++++++++------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index 6544c37..1f6922b 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -17,26 +17,9 @@ def construct_internal_namespace(data, block): class SteadyStateDict: def __init__(self, data, internal=None): - if isinstance(data, SteadyStateDict): - self.internal = deepcopy(data.internal) - self.toplevel = deepcopy(data.toplevel) - else: - if internal is not None: - # Either we can construct the internal namespace for you (if it's a Block) otherwise, - # you can provide the nested dict representing the internal namespace directly - if hasattr(internal, "inputs") and hasattr(internal, "outputs"): - self.internal = construct_internal_namespace(data, internal) - else: - self.internal = internal - else: - self.internal = {} - if self.internal: - toplevel = deepcopy(data) - for internal_dict in self.internal.values(): - toplevel = dict_diff(toplevel, internal_dict) - self.toplevel = toplevel - else: - self.toplevel = deepcopy(data) + self.toplevel = {} + self.internal = {} + self.update(data, internal=internal) def __repr__(self): if self.internal: @@ -68,14 +51,26 @@ def values(self): def items(self): return self.toplevel.items() - def update(self, new_data): - if isinstance(new_data, SteadyStateDict): - self.toplevel.update(new_data.toplevel) - self.internal.update(new_data.internal) + def update(self, data, internal=None): + if isinstance(data, SteadyStateDict): + self.internal.update(deepcopy(data.internal)) + self.toplevel.update(deepcopy(data.toplevel)) else: - # TODO: This is assuming new_data only contains aggregates. Upgrade in later commit to handle the case of - # vector-valued variables/collection into internal namespaces - self.toplevel.update(new_data) + toplevel = deepcopy(data) + if internal is not None: + # Either we can construct the internal namespace for you (if it's a Block) otherwise, + # you can provide the nested dict representing the internal namespace directly + if hasattr(internal, "inputs") and hasattr(internal, "outputs"): + internal = construct_internal_namespace(data, internal) + + # Remove the internal data from `data` if it's there + for internal_dict in internal.values(): + toplevel = dict_diff(toplevel, internal_dict) + + self.toplevel.update(toplevel) + self.internal.update(internal) + else: + self.toplevel.update(toplevel) def difference(self, data_to_remove): return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) From 46eb0ce666ef01e3a5f2e24b7becd5b5a1a67ec7 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:47:35 -0500 Subject: [PATCH 124/288] Move impulse dimension unifying in SimpleBlock out to a separate function as opposed to a hidden block method --- sequence_jacobian/blocks/simple_block.py | 35 +++++++++--------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index 7224710..fa735ae 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -67,27 +67,6 @@ def steady_state(self, calibration): output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [misc.numeric_primitive(self.f(**input_args))] return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) - # TODO: Remove this so everything is just done within impulse_nonlinear - def _output_in_td_format(self, **kwargs_new): - """Returns output of the method td as a dict mapping output names to numeric primitives (scalars/vectors) - or a single numeric primitive of output values, as opposed to Ignore/IgnoreVector/Displace objects. - - Also accounts for the fact that for outputs of block.td that were *not* affected by a Displace object, i.e. - variables that remained at their ss value in spite of other variables within that same block being - affected by the Displace object (e.g. I in the mkt_clearing block of the two_asset model - is unchanged by a shock to rstar, being only a function of K's ss value and delta), - we still want to return them as paths (i.e. vectors, if they were - previously scalars) to impose uniformity on the dimensionality of the td returned values. - """ - out = self.f(**kwargs_new) - if len(self.output_list) > 1: - # Because we know at least one of the outputs in `out` must be of length T - T = np.max([np.size(o) for o in out]) - out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else misc.numeric_primitive(o) for o in out] - return dict(zip(self.output_list, misc.make_tuple(out_unif_dim))) - else: - return dict(zip(self.output_list, misc.make_tuple(misc.numeric_primitive(out)))) - def impulse_nonlinear(self, ss, exogenous): input_args = {} for k, v in exogenous.items(): @@ -99,7 +78,7 @@ def impulse_nonlinear(self, ss, exogenous): if k not in input_args: input_args[k] = ignore(ss[k]) - return ImpulseDict(self._output_in_td_format(**input_args), ss) + return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list), ss) def impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous), ss) @@ -175,3 +154,15 @@ def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): J[o_name] = SimpleSparse(o.elements) return J + + +def make_impulse_uniform_length(out, output_list): + # If the function has multiple outputs + if isinstance(out, tuple): + # Because we know at least one of the outputs in `out` must be of length T + T = np.max([np.size(o) for o in out]) + out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else + misc.numeric_primitive(o) for o in out] + return dict(zip(output_list, misc.make_tuple(out_unif_dim))) + else: + return dict(zip(output_list, misc.make_tuple(misc.numeric_primitive(out)))) From 6017780f670148173974f7058abb126fc73c30c2 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Sun, 25 Apr 2021 15:48:33 -0500 Subject: [PATCH 125/288] Change robust steady state solver for hank to be hybr instead of broyden1 since it's more stable --- tests/robustness/test_steady_state.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index adc4d02..56e1c96 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -19,8 +19,7 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): unknowns_ss = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10.), "w": 0.8} targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} ss_ref = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, helper_targets=["nkpc_res"], - solver="broyden1", solver_kwargs={"options": {"maxiter": 300}}, + helper_blocks=helper_blocks, helper_targets=["nkpc_res"], solver="hybr", constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) for k in ss.keys(): From 6c4661f3c62502b62b58a6b156e8d54475af6729 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 29 Apr 2021 20:23:18 -0500 Subject: [PATCH 126/288] Change internal namespace construction for HetBlock to directly access the .internal attr of the HetBlock --- sequence_jacobian/blocks/het_block.py | 8 ++++++ sequence_jacobian/steady_state/classes.py | 30 ++++++++--------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 60ca221..ed27602 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -103,6 +103,10 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.hetoutput_outputs = set() self.hetoutput_outputs_order = tuple() + # The set of variables that will be wrapped in a separate namespace for this HetBlock + # as opposed to being available at the top level + self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} + if len(self.policy) > 2: raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") @@ -467,6 +471,8 @@ def add_hetinput(self, hetinput, overwrite=False, verbose=True): self.inputs |= self.hetinput_inputs self.inputs -= self.hetinput_outputs + self.internal |= self.hetinput_outputs + def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process inputs through the hetoutput function. @@ -503,6 +509,8 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): # Modify the HetBlock's outputs to include the aggregated hetoutputs self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) + self.internal |= self.hetoutput_outputs + '''Part 3: components of ss(): - policy_ss : backward iteration to get steady-state policies and other outcomes - dist_ss : forward iteration to get steady-state distribution and compute aggregates diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py index 1f6922b..cb8238f 100644 --- a/sequence_jacobian/steady_state/classes.py +++ b/sequence_jacobian/steady_state/classes.py @@ -2,24 +2,14 @@ from copy import deepcopy -from ..utilities.misc import dict_diff, smart_set - - -def construct_internal_namespace(data, block): - # Only supporting internal namespaces for HetBlocks currently - if hasattr(block, "back_step_fun"): - return {block.name: {k: v for k, v in deepcopy(data).items() if k in - smart_set(block.back_step_outputs) | smart_set(block.exogenous) | {"D"} | - smart_set(block.hetinput_outputs) | smart_set(block.hetoutput_outputs)}} - else: - return {} +from ..utilities.misc import dict_diff class SteadyStateDict: def __init__(self, data, internal=None): self.toplevel = {} self.internal = {} - self.update(data, internal=internal) + self.update(data, internal_namespaces=internal) def __repr__(self): if self.internal: @@ -51,24 +41,24 @@ def values(self): def items(self): return self.toplevel.items() - def update(self, data, internal=None): + def update(self, data, internal_namespaces=None): if isinstance(data, SteadyStateDict): self.internal.update(deepcopy(data.internal)) self.toplevel.update(deepcopy(data.toplevel)) else: toplevel = deepcopy(data) - if internal is not None: - # Either we can construct the internal namespace for you (if it's a Block) otherwise, - # you can provide the nested dict representing the internal namespace directly - if hasattr(internal, "inputs") and hasattr(internal, "outputs"): - internal = construct_internal_namespace(data, internal) + if internal_namespaces is not None: + # Construct the internal namespace from the Block object, if a Block is provided + if hasattr(internal_namespaces, "internal"): + internal_namespaces = {internal_namespaces.name: {k: v for k, v in deepcopy(data).items() if k in + internal_namespaces.internal}} # Remove the internal data from `data` if it's there - for internal_dict in internal.values(): + for internal_dict in internal_namespaces.values(): toplevel = dict_diff(toplevel, internal_dict) self.toplevel.update(toplevel) - self.internal.update(internal) + self.internal.update(internal_namespaces) else: self.toplevel.update(toplevel) From 48913b6c068b4aaece06f89c8f8a7c578af761eb Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 29 Apr 2021 20:50:54 -0500 Subject: [PATCH 127/288] Add SteadyStateDict unit test --- tests/base/test_public_classes.py | 49 ++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 5a048a8..e37eea6 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -1,10 +1,57 @@ """Test public-facing classes""" import numpy as np -from sequence_jacobian import create_model + +from sequence_jacobian import het +from sequence_jacobian.steady_state.classes import SteadyStateDict from sequence_jacobian.blocks.support.impulse import ImpulseDict +def test_steadystatedict(): + toplevel = {"A": 1., "B": 2.} + internal = {"block1": {"a": np.array([0, 1]), "D": np.array([[0, 0.5], [0.5, 0]]), + "Pi": np.array([[0.5, 0.5], [0.5, 0.5]])}} + raw_output = {"A": 1., "B": 2., "a": np.array([0, 1]), "D": np.array([[0, 0.5], [0.5, 0]]), + "Pi": np.array([[0.5, 0.5], [0.5, 0.5]])} + + @het(exogenous="Pi", policy="a", backward="Va") + def block1(Va_p, Pi_p, t1, t2): + Va = Va_p + a = t1 + t2 + return Va, a + + ss1 = SteadyStateDict(toplevel, internal=internal) + ss2 = SteadyStateDict(raw_output, internal=block1) + + # Test that both ways of instantiating SteadyStateDict given by ss1 and ss2 are equivalent + def check_steady_states(ss1, ss2): + assert set(ss1.keys()) == set(ss2.keys()) + for k in ss1: + assert np.isclose(ss1[k], ss2[k]) + + assert set(ss1.internal.keys()) == set(ss2.internal.keys()) + for k in ss1.internal: + assert set(ss1.internal[k].keys()) == set(ss2.internal[k].keys()) + for kk in ss1.internal[k]: + assert np.all(np.isclose(ss1.internal[k][kk], ss2.internal[k][kk])) + + check_steady_states(ss1, ss2) + + # Test iterable indexing + assert ss1[["A", "B"]] == {"A": 1., "B": 2.} + + # Test updating + toplevel_new = {"C": 2., "D": 4.} + internal_new = {"block1_new": {"a": np.array([2, 0]), "D": np.array([[0.25, 0.25], [0.25, 0.25]]), + "Pi": np.array([[0.2, 0.8], [0.8, 0.2]])}} + ss_new = SteadyStateDict(toplevel_new, internal=internal_new) + + ss1.update(ss_new) + ss2.update(toplevel_new, internal_namespaces=internal_new) + + check_steady_states(ss1, ss2) + + def test_impulsedict(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 200 From 92d21e19d9bee94c1b0538f62a4ccb145e16a936 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 30 Apr 2021 13:49:36 -0500 Subject: [PATCH 128/288] Fix typo in two_asset steady state targets --- tests/conftest.py | 4 ++-- tests/robustness/test_steady_state.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index dfcbabc..552eed2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,10 +90,10 @@ def two_asset_hank_dag(): "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} - targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} + targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", helper_blocks=helper_blocks, - helper_targets=["wnkpc", "pi", "K", "wealth", "N"]) + helper_targets=["wnkpc", "piw", "K", "wealth", "N"]) # Transitional Dynamics/Jacobian Calculation exogenous = ["rstar", "Z", "G"] diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index 56e1c96..7176151 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -38,10 +38,10 @@ def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} - targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'pi': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} + targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} ss_ref = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", helper_blocks=helper_blocks, - helper_targets={'wnkpc': 0., 'pi': 0.0, "K": 10., + helper_targets={'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0}) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) From 45fb98d20774820975639082fd06f2de104d1500 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 30 Apr 2021 17:00:17 -0500 Subject: [PATCH 129/288] Update notebooks to be compatible with the new API --- notebooks/hank.ipynb | 665 +++------------------ notebooks/het_jacobian.ipynb | 168 +++--- notebooks/intro_to_sequence_jacobian.ipynb | 2 + notebooks/krusell_smith.ipynb | 330 +++++----- notebooks/rbc.ipynb | 205 +++---- notebooks/two_asset.ipynb | 238 +++----- 6 files changed, 462 insertions(+), 1146 deletions(-) diff --git a/notebooks/hank.ipynb b/notebooks/hank.ipynb index a52cc83..8871724 100644 --- a/notebooks/hank.ipynb +++ b/notebooks/hank.ipynb @@ -10,7 +10,6 @@ "2. [Solve for a steady state with multiple calibration targets](#2-calibration)\n", "3. [Compute linearized impulse responses: unwrap convenience function](#3-linear)\n", "4. [Compute nonlinear impulse responses: quasi-Newton performs well even for large nonlinearities](#4-nonlinear)\n", - "5. [Check local determinacy](#5-determinacy)\n", "\n", "This notebook accompanies the working paper by Auclert, Bardóczy, Rognlie, Straub (2019): \"Using the Sequence-Space Jacobian to Solve and Estimate Heterogeneous-Agent Models\". Please see the [Github repository](https://github.com/shade-econ/sequence-jacobian) for more information and code.\n", "\n", @@ -89,8 +88,8 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj\n", - "from sequence_jacobian import simple, het" + "from sequence_jacobian import simple, het, create_model\n", + "from sequence_jacobian.models import hank" ] }, { @@ -133,7 +132,10 @@ "metadata": {}, "outputs": [], "source": [ - "def transfers(pi_e, Div, Tax, e_grid, div_rule, tax_rule): \n", + "def transfers(pi_e, Div, Tax, e_grid):\n", + " # default incidence rules are proportional to skill\n", + " tax_rule, div_rule = e_grid, e_grid # scale does not matter, will be normalized anyway\n", + "\n", " div = Div / np.sum(pi_e * div_rule) * div_rule\n", " tax = Tax / np.sum(pi_e * tax_rule) * tax_rule\n", " T = div - tax\n", @@ -155,8 +157,8 @@ "metadata": {}, "outputs": [], "source": [ - "sj.hank.household.add_hetinput(transfers, overwrite=True, verbose=False)\n", - "household = sj.hank.household" + "household = hank.household\n", + "household.add_hetinput(transfers, overwrite=True, verbose=False)" ] }, { @@ -173,77 +175,26 @@ "\n", "\n", "## 2 Calibrating the steady state\n", - "Similarly to the RBC example, we calibrate the discount factor $\\beta$ and disutility of labor $\\varphi$ to hit a target for the interest rate and effective labor $L=1.$\n", - "\n", - "This is a two-dimensional rootfinding problem that we solve by Broyden's method, which we implemented in ``utilities/solvers.py``. It takes a function $f: \\mathbb{R}^n \\to \\mathbb{R}^n$ and an initial guess for its roots, $x_0 \\in \\mathbb{R}^n$, and backtracks whenever $f$ returns a `ValueError`.\n", - "\n", - "The calibration has two substantive steps. First, express analytically all variables that don't depend on $(\\beta, \\varphi).$ Second, construct the residual function that takes the current guesses $(\\beta, \\varphi)$ and maps them into deviations from the calibration targets. This just requires an evaluation of the household block. The rootfinder does the rest. \n", - "\n", - "Although additional efficiency gains would be possible here (for instance, by updating our initial guesses for policy and distribution along the way), we will not implement them, since they are not our focus here." + "Similarly to the RBC example, we calibrate the discount factor $\\beta$ and disutility of labor $\\varphi$ to hit a target for the interest rate and effective labor $L=1.$ Additionally we calibrate the wage $w$ such that the Phillips curve relation is satisfied in steady state." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1.2, B_Y=5.6, rho_s=0.966, sigma_s=0.5,\n", - " kappa=0.1, phi=1.5, nS=7, amax=150, nA=500, tax_rule=None, div_rule=None):\n", - " \"\"\"Solve steady state of full GE model. Calibrate (beta, vphi) to hit target for interest rate and Y.\"\"\"\n", - "\n", - " # set up grid\n", - " a_grid = sj.utilities.discretize.agrid(amax=amax, n=nA)\n", - " e_grid, pi_e, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS)\n", - " \n", - " # default incidence rules are proportional to skill\n", - " if tax_rule is None:\n", - " tax_rule = e_grid # scale does not matter, will be normalized anyway\n", - " if div_rule is None:\n", - " div_rule = e_grid\n", - " assert len(tax_rule) == len(div_rule) == len(e_grid), 'Incidence rules are inconsistent with income grid.'\n", - "\n", - " # solve analytically what we can\n", - " B = B_Y\n", - " w = 1 / mu\n", - " Div = (1 - w)\n", - " Tax = r * B\n", - " T = transfers(pi_e, Div, Tax, e_grid, div_rule, tax_rule)\n", - "\n", - " # initialize guess for policy function iteration\n", - " fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0]\n", - " coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis]\n", - " Va = (1 + r) * (0.1 * coh) ** (-1 / eis)\n", - "\n", - " # residual function\n", - " def res(x):\n", - " beta_loc, vphi_loc = x\n", - " # precompute constrained c and n which don't depend on Va\n", - " c_const_loc, n_const_loc = sj.hank.solve_cn(w * e_grid[:, np.newaxis], fininc, eis, frisch, vphi_loc, Va)\n", - " if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001:\n", - " raise ValueError('Clearly invalid inputs')\n", - " out = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta_loc, eis=eis,\n", - " Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc, c_const=c_const_loc, n_const=n_const_loc,\n", - " tax_rule=tax_rule, div_rule=div_rule, ssflag=True)\n", - " return np.array([out['A'] - B, out['N_e'] - 1])\n", + "blocks = [household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc,\n", + " hank.income_state_vars, hank.asset_state_vars]\n", + "hank_model = create_model(blocks, name=\"One Asset HANK\")\n", "\n", - " # solve for beta, vphi\n", - " (beta, vphi), _ = sj.utilities.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False)\n", + "calibration = {\"r\": 0.005, \"rstar\": 0.005, \"eis\": 0.5, \"frisch\": 0.5, \"B_Y\": 5.6, \"B\": 5.6, \"mu\": 1.2,\n", + " \"rho_s\": 0.966, \"sigma_s\": 0.5, \"kappa\": 0.1, \"phi\": 1.5, \"Y\": 1, \"Z\": 1, \"L\": 1,\n", + " \"pi\": 0, \"nS\": 7, \"amax\": 150, \"nA\": 500}\n", + "unknowns_ss = {\"beta\": 0.986, \"vphi\": 0.8, \"w\": 0.8}\n", + "targets_ss = {\"asset_mkt\": 0, \"labor_mkt\": 0, \"nkpc_res\": 0.}\n", "\n", - " # extra evaluation for reporting\n", - " c_const, n_const = sj.hank.solve_cn(w * e_grid[:, np.newaxis], fininc, eis, frisch, vphi, Va)\n", - " ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis,\n", - " Div=Div, Tax=Tax, frisch=frisch, vphi=vphi, c_const=c_const, n_const=n_const,\n", - " tax_rule=tax_rule, div_rule=div_rule, ssflag=True)\n", - " \n", - " # check Walras's law\n", - " walras = 1 - ss['C']\n", - " assert np.abs(walras) < 1E-8\n", - " \n", - " # add aggregate variables\n", - " ss.update({'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0,\n", - " 'walras': walras, 'ssflag': False})\n", - " return ss" + "ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { @@ -255,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -272,8 +223,7 @@ } ], "source": [ - "ss = hank_ss()\n", - "plt.plot(ss['a_grid'], ss['n'].T)\n", + "plt.plot(ss['a_grid'], ss.internal[\"household\"]['n'].T)\n", "plt.xlabel('Assets'), plt.ylabel('Labor supply')\n", "plt.show()" ] @@ -318,7 +268,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.1 Define simple blocks" + "### 3.1 Cut to the chase\n", + "The recommended way to obtain the general equilibrium Jacobians is to use the `solve_jacobian` method for the `hank_model` object." ] }, { @@ -326,50 +277,6 @@ "execution_count": 6, "metadata": {}, "outputs": [], - "source": [ - "@simple\n", - "def firm(Y, w, Z, pi, mu, kappa):\n", - " L = Y / Z\n", - " Div = Y - w * L - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y\n", - " return L, Div\n", - "\n", - "@simple\n", - "def monetary(pi, rstar, phi):\n", - " r = (1 + rstar(-1) + phi * pi(-1)) / (1 + pi) - 1\n", - " return r\n", - "\n", - "@simple\n", - "def fiscal(r, B):\n", - " Tax = r * B\n", - " return Tax\n", - "\n", - "@simple\n", - "def mkt_clearing(A, N_e, C, L, Y, B, pi, mu, kappa):\n", - " asset_mkt = A - B\n", - " labor_mkt = N_e - L\n", - " goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y\n", - " return asset_mkt, labor_mkt, goods_mkt\n", - "\n", - "@simple\n", - "def nkpc(pi, w, Z, Y, r, mu, kappa):\n", - " nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y *\\\n", - " (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log)\n", - " return nkpc_res" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3.2 Cut to the chase\n", - "The surest way to obtain the general equilibrium Jacobians is to use the `get_G` convenience function. Notice the `save=True` option. This means that we're saving the HA Jacobians calculated along the way for later use." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], "source": [ "# setup\n", "T = 300\n", @@ -378,17 +285,16 @@ "targets = ['nkpc_res', 'asset_mkt', 'labor_mkt']\n", "\n", "# general equilibrium jacobians\n", - "block_list = [firm, monetary, fiscal, nkpc, mkt_clearing, household] \n", - "G = sj.get_G(block_list, exogenous, unknowns, targets, T, ss, save=True)" + "G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.3 Break down `get_G`\n", + "### 3.2 Break down `solve_jacobian`\n", "\n", - "Under the hood, the very powerful `jac.get_G` performs the following steps:\n", + "Under the hood, the `solve_jacobian` method performs the following steps:\n", " - orders the blocks so that we move forward along the model's DAG\n", " - computes the partial Jacobians $\\mathcal{J}^{o,i}$ from all blocks (if their Jacobian is not supplied already), only with respect to the inputs that actually change: unknowns, exogenous shocks, outputs of earlier blocks\n", " - forward accumulates partial Jacobians $\\mathcal{J}^{o,i}$ to form total Jacobians $\\mathbf{J}^{o,i}$\n", @@ -409,58 +315,61 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ - "curlyJs, required = sj.jacobian.curlyJ_sorted(block_list, unknowns+exogenous, ss, T)" + "import sequence_jacobian.jacobian.drivers as jacobian\n", + "from sequence_jacobian.jacobian.classes import JacobianDict\n", + "\n", + "curlyJs, required = jacobian.curlyJ_sorted(blocks, unknowns + exogenous, ss, T)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The first output `curlyJs` is a list of nested dictionaries. Each entry in the list contains all the necessary Jacobians for the corresponding block. Blocks are ordered according to the topological sort.\n", + "The first output `curlyJs` is a list of `JacobianDict` objects. Each `JacobianDict` contains all the necessary Jacobians for the corresponding block. Blocks are ordered according to the topological sort.\n", "\n", "For example, the first block is `monetary`, because it only takes an unknown $\\pi$ and an exogenous $r^*$ as inputs. Let's take a look. " ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'r': {'pi': SimpleSparse({(-1, 0): 1.500, (0, 0): -1.005}), 'rstar': SimpleSparse({(-1, 0): 1.000})}}\n" + "The JacobianDict for the monetary block is: \n" ] } ], "source": [ - "print(curlyJs[0])" + "print(f\"The JacobianDict for the monetary block is: {curlyJs[0]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Since this is a simple block, the Jacobians are represented as a instances of the `SimpleSparse` class. Note that `jac.curlyJ_sorted` correctly determined that it is not necessary to differentiate with respect to the Taylor rule parameter $\\phi$ (if we wanted to consider shocks to this parameter, we'd just have to include it among the exogenous inputs.)\n", + "Note that `curlyJ_sorted` correctly determined that it is not necessary to differentiate with respect to the Taylor rule parameter $\\phi$ (if we wanted to consider shocks to this parameter, we'd just have to include it among the exogenous inputs.)\n", "\n", "The second output `required` is a set of extra variables (not unknowns and exogenous) that we have to differentiate with respect to, because they are outputs of some blocks and inputs of others. " ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'r', 'Tax', 'Div', 'C', 'A', 'N_e', 'L'}\n" + "{'A', 'C', 'N_e', 'r', 'Div', 'L', 'Tax'}\n" ] } ], @@ -480,23 +389,12 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_keys(['nkpc_res', 'asset_mkt', 'labor_mkt'])\n", - "dict_keys(['pi', 'Y', 'w'])\n" - ] - } - ], + "outputs": [], "source": [ - "J_curlyH_U = sj.jacobian.forward_accumulate(curlyJs, unknowns, targets, required)\n", - "J_curlyH_Z = sj.jacobian.forward_accumulate(curlyJs, exogenous, targets, required)\n", - "print(J_curlyH_U.keys())\n", - "print(J_curlyH_U['asset_mkt'].keys())" + "J_curlyH_U = jacobian.forward_accumulate(curlyJs, unknowns, targets, required)\n", + "J_curlyH_Z = jacobian.forward_accumulate(curlyJs, exogenous, targets, required)" ] }, { @@ -508,7 +406,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -521,8 +419,8 @@ } ], "source": [ - "H_U = sj.jacobian.pack_jacobians(J_curlyH_U, unknowns, targets, T)\n", - "H_Z = sj.jacobian.pack_jacobians(J_curlyH_Z, exogenous, targets, T)\n", + "H_U = J_curlyH_U[targets, unknowns].pack(T)\n", + "H_Z = J_curlyH_Z[targets, exogenous].pack(T)\n", "print(H_U.shape)\n", "print(H_Z.shape)" ] @@ -537,20 +435,11 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 17, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dict_keys(['pi', 'w', 'Y'])\n" - ] - } - ], + "outputs": [], "source": [ - "G_U = sj.jacobian.unpack_jacobians(-np.linalg.solve(H_U, H_Z), exogenous, unknowns, T)\n", - "print(G_U.keys())" + "G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T)" ] }, { @@ -562,27 +451,27 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ - "curlyJs = [G_U] + curlyJs\n", - "outputs = set().union(*(curlyJ.keys() for curlyJ in curlyJs)) - set(targets)\n", + "curlyJs_aug = [G_U] + curlyJs\n", + "outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs_aug)) - set(targets)\n", "\n", - "G2 = sj.jacobian.forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns))" + "G2 = jacobian.forward_accumulate(curlyJs_aug, exogenous, outputs, required | set(unknowns))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.4 Results\n", + "### 3.3 Results\n", "First let's check that we have correctly reconstructed the steps of `jac.get_G`." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -600,12 +489,12 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -649,7 +538,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -677,24 +566,7 @@ " max error for asset_mkt is 5.01E-10\n", " max error for labor_mkt is 1.26E-11\n" ] - } - ], - "source": [ - "rho_r, sig_r = 0.61, -0.01/4\n", - "drstar = sig_r * rho_r ** (np.arange(T))\n", - "rstar = ss['r'] + drstar\n", - "\n", - "H_U = sj.get_H_U(block_list, unknowns, targets, T, ss, use_saved=True)\n", - "H_U_factored = sj.utilities.misc.factor(H_U)\n", - "\n", - "td_nonlin = sj.td_solve(ss, block_list, unknowns, targets, H_U_factored=H_U_factored,rstar=rstar)" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ + }, { "data": { "image/png": "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\n", @@ -709,8 +581,14 @@ } ], "source": [ - "dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C']\n", - "dC_nonlin = 100 * (td_nonlin['C']/ss['C'] - 1) \n", + "rho_r, sig_r = 0.61, -0.01/4\n", + "rstar_shock_path = {\"rstar\": sig_r * rho_r ** (np.arange(T))}\n", + "\n", + "td_nonlin = hank_model.solve_impulse_nonlinear(ss, rstar_shock_path, unknowns, targets)\n", + "td_lin = hank_model.solve_impulse_linear(ss, rstar_shock_path, unknowns, targets)\n", + "\n", + "dC_nonlin = 100 * td_nonlin.deviations().normalize()[\"C\"]\n", + "dC_lin = 100 * td_lin.normalize()[\"C\"]\n", "\n", "plt.plot(dC_lin[:21], label='linear', linestyle='-', linewidth=2.5)\n", "plt.plot(dC_nonlin[:21], label='nonlinear', linestyle='--', linewidth=2.5)\n", @@ -731,7 +609,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -795,13 +673,13 @@ ], "source": [ "rho_r, sig_r = 0.61, -0.10/4\n", - "drstar = sig_r * rho_r ** (np.arange(T))\n", - "rstar = ss['r'] + drstar\n", + "rstar_shock_path = {\"rstar\": sig_r * rho_r ** (np.arange(T))}\n", "\n", - "td_nonlin = sj.td_solve(ss, block_list, unknowns, targets, H_U_factored=H_U_factored, rstar=rstar)\n", + "td_nonlin = hank_model.solve_impulse_nonlinear(ss, rstar_shock_path, unknowns, targets)\n", + "td_lin = hank_model.solve_impulse_linear(ss, rstar_shock_path, unknowns, targets)\n", "\n", - "dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C']\n", - "dC_nonlin = 100 * (td_nonlin['C']/ss['C'] - 1) \n", + "dC_nonlin = 100 * td_nonlin.deviations().normalize()[\"C\"]\n", + "dC_lin = 100 * td_lin.normalize()[\"C\"]\n", "\n", "plt.plot(dC_lin[:21], label='linear', linestyle='-', linewidth=2.5)\n", "plt.plot(dC_nonlin[:21], label='nonlinear', linestyle='--', linewidth=2.5)\n", @@ -811,405 +689,6 @@ "plt.legend()\n", "plt.show()" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "## 5. Local determinacy\n", - "Local determinacy boils down to the invertibility of the matrix $H_U$. The steady state is a locally-determinate equilibrium if and only if $H_U$ is invertible. \n", - "\n", - "**Numerical approach.** In practice, $H_U$ is obtained numerically for a finite horizon, and thus we can never expect it to be exactly singular, even if equilibrium is indeterminate. Still, near-singularity of $H_U$, especially when it becomes more drastic as the truncation horizon $T$ is increased, is a likely indication of indeterminacy.\n", - "\n", - "In practice, we have found that indeterminacy is best detected by looking at the last few singular values: if the smallest is discontinuously smaller than the second and third smallest, then indeterminacy is likely.\n", - "\n", - "**Our contribution: winding number criterion.** A better solution is to use the winding number criterion introduced in our paper, which rapidly gives an exact answer. This criterion exploits the \"asymptotic time invariant\" structure of the Jacobians in SHADE models: within each Jacobian, each diagonal eventually converges to some constant, and these constants are close to zero far enough away from the main diagonal.\n", - "\n", - "Given knowledge of the asymptotic structure of $H_U$, which is encoded in an array $A$, the criterion calculates the \"winding number\" of the curve\n", - "\n", - "$$\n", - "\\det A(\\lambda) = \\det\\sum_{j=-\\infty}^\\infty A_j e^{ij\\lambda} \\tag{1}\n", - "$$\n", - "\n", - "as $\\lambda$ varies from $0$ to $2\\pi$. Here, $A_j$ is the $n_u\\times n_u$ matrix representing the asymptotic value on the $j$th diagonal above the main diagonal for all pairs of targets and unknowns. The \"winding number\" is the number of times the curve (1) wraps counterclockwise around the origin in the complex plane.\n", - "\n", - "A winding number of 0 indicates that the model has a unique solution around the steady state, while a winding number of -1 or less indicates indeterminacy.\n", - "\n", - "**Example in our HANK model.** As it is well-known, determinacy in the New Keynesian models requires that the interest rate rule is sufficiently responsive to inflation. Therefore, we're going to illustrate the issue by varying the parameter $\\phi$ and tracing its effect on $H_U.$\n", - "\n", - "### 5.1 Stable case\n", - "Let's start with the the baseline calibration with $\\phi=1.5$. Both approaches show the model is determinate, as expected." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Smallest singular values: 0.0720, 0.0715, 0.0715\n", - "Winding number: 0\n" - ] - } - ], - "source": [ - "# smallest singular values\n", - "_, s, _ = np.linalg.svd(H_U)\n", - "print(f'Smallest singular values: {s[-3]:.4f}, {s[-2]:.4f}, {s[-1]:.4f}')\n", - "\n", - "# winding number test\n", - "# first, use get_H_U with asymptotic=True to get array A representing asymptotic H_U\n", - "A = sj.get_H_U(block_list, unknowns, targets, T, ss, asymptotic=True, save=True, use_saved=True)\n", - "\n", - "# then apply winding number criterion\n", - "wn = sj.determinacy.winding_criterion(A)\n", - "print(f'Winding number: {wn}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.2 Unstable case\n", - "Let's see what happens with $\\phi=0.75$. First of all, we'll have to recompute the Jacobian. It's important to realize that $\\phi$ does not affect the steady state, and affects dynamics only through the monetary block. Thus, recomputing the Jacobians of the household block would be wasteful. We can avoid this by setting ``use_saved=True``. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "ss2 = {**ss, 'phi': 0.75}\n", - "H_U2 = sj.get_H_U(block_list, unknowns, targets, T, ss2, use_saved=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This time both tests reveal clear indeterminacy: the smallest singular value is discontinuously smaller than the others, and the winding number is -1." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Smallest singular values: 0.0967, 0.0960, 0.0000\n", - "Winding number: -1\n" - ] - } - ], - "source": [ - "# smallest singular values\n", - "_, s2, _ = np.linalg.svd(H_U2)\n", - "print(f'Smallest singular values: {s2[-3]:.4f}, {s2[-2]:.4f}, {s2[-1]:.4f}')\n", - "\n", - "# winding number\n", - "A2 = sj.get_H_U(block_list, unknowns, targets, T, ss2, asymptotic=True, use_saved=True)\n", - "wn2 = sj.determinacy.winding_criterion(A2)\n", - "print(f'Winding number: {wn2}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Not surprisingly, if we tried to use this Jacobian to compute impulse responses, we'd fail. (We'll wrap in a try/except block to avoid a giant error message.)" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "On iteration 0\n", - " max error for nkpc_res is 0.00E+00\n", - " max error for asset_mkt is 1.41E-01\n", - " max error for labor_mkt is 2.68E-02\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\michaelcai\\PycharmProjects\\sequence-jacobian\\sequence_jacobian\\blocks\\support\\simple_displacement.py:207: RuntimeWarning: invalid value encountered in log\n", - " return Displace(f(numeric_primitive(self), **kwargs), ss=f(self.ss))\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "On iteration 1\n", - " max error for nkpc_res is NAN\n", - " max error for asset_mkt is NAN\n", - " max error for labor_mkt is NAN\n", - "array must not contain infs or NaNs\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\michaelcai\\PycharmProjects\\sequence-jacobian\\sequence_jacobian\\models\\hank.py:41: RuntimeWarning: invalid value encountered in less\n", - " iconst = np.nonzero(a < a_grid[0])\n" - ] - } - ], - "source": [ - "try:\n", - " td_nonlin = sj.td_solve(ss2, block_list, unknowns, targets, H_U=H_U2, rstar=rstar)\n", - "except ValueError as e:\n", - " print(e)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In fact, it fails after the first iteration: since the Jacobian is nearly singular, using its inverse in Newton's method leads to a very large step to the next guess, which then is outside the admissible domain and leads to an error within the household routine." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.3 Why use the winding number criterion?\n", - "\n", - "It's very fast and precise. We can use bisection, for instance, to get the exact threshold at which the model becomes determinate. It turns out that this is at approximately $\\phi=1.005$." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Threshold for determinacy: phi=1.005\n" - ] - } - ], - "source": [ - "phi_low = 0.8\n", - "phi_high = 1.2\n", - "while phi_high - phi_low > 1E-6:\n", - " phi_mid = (phi_low + phi_high)/2\n", - " ss_cur = {**ss, 'phi': phi_mid}\n", - " A_cur = sj.get_H_U(block_list, unknowns, targets, T, ss_cur,\n", - " asymptotic=True, use_saved=True)\n", - " wn_cur = sj.determinacy.winding_criterion(A_cur)\n", - " if wn_cur == 0:\n", - " phi_high = phi_mid\n", - " else:\n", - " phi_low = phi_mid\n", - "phi_threshold = (phi_low + phi_high)/2\n", - "print(f'Threshold for determinacy: phi={phi_threshold:.3f}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can compare to the results from looking at singular values. Specifically, we'll look at the ratio of the smallest to the second-smallest singular value for a range of $\\phi$ around the determinacy threshold we've identified.\n", - "\n", - "This takes several seconds, because the singular value decomposition is costly and we need to redo it for every $\\phi$." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "# non-uniform grid of phis to get extra precision near\n", - "# where we know from winding number test the threshold lies\n", - "phis = np.unique(np.concatenate((np.linspace(0.99, 1.00, 5),\n", - " np.linspace(1.00, 1.01, 10),\n", - " np.linspace(1.01, 1.02, 5))))\n", - "\n", - "sv_ratio = np.empty_like(phis)\n", - "for it, phi in enumerate(phis):\n", - " ss_cur = {**ss, 'phi': phi}\n", - " H_U_cur = sj.get_H_U(block_list, unknowns, targets, T, ss_cur, use_saved=True)\n", - "\n", - " _, s, _ = np.linalg.svd(H_U_cur)\n", - " sv_ratio[it] = s[-1] / s[-2]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's plot this ratio against the winding number plus 1, which jumps up at the determinacy threshold we've already calculated." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# winding number plus 1 jumps up at phi_threshold\n", - "phis_wn = [phis[0], phi_threshold, phi_threshold, phis[-1]]\n", - "wns = [0, 0, 1, 1]\n", - "\n", - "plt.plot(phis_wn, wns, linewidth=2, label=r'winding number + 1')\n", - "plt.plot(phis, sv_ratio, linewidth=2, label=r'singular value ratio', linestyle='--')\n", - "plt.legend(framealpha=0)\n", - "plt.xlabel(r'Taylor rule coefficient $\\phi$');" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We see that the two approaches give consistent answers, but the winding number approach is far more precise and immediate." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5.4 Visualizing the winding number criterion\n", - "To see how this works, we can also directly plot the curve $\\det A(\\lambda)$ for which we're taking the winding number. The function `det.detA_path`, which is called by `det.winding_criterion` under the hood, provides this.\n", - "\n", - "**Indeterminate case.** First let's do so for an indeterminate case $\\phi=1.001$." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "phi = 1.001\n", - "ss_cur = {**ss, 'phi': phi}\n", - "A_cur = sj.get_H_U(block_list, unknowns, targets, T, ss_cur,\n", - " asymptotic=True, use_saved=True)\n", - "\n", - "det_Alambda = sj.determinacy.detA_path(A_cur)\n", - "x, y = det_Alambda.real, det_Alambda.imag\n", - "\n", - "# plot curve\n", - "plt.plot(x, y, label=r'$\\det A(\\lambda)$', linewidth=3);\n", - "\n", - "# dot for origin\n", - "plt.plot(0, 0, marker='o', markersize=5, color=\"black\")\n", - "\n", - "# arrow to show orientation (using rate of change around lambda=0)\n", - "plt.arrow(x[0], y[0], 0.001*(x[1]-x[-2]), 0.001*(y[1]-y[-2]), color='C0',\n", - " width=0.0001, head_width=0.05, head_length=0.08)\n", - "plt.title(r'Indeterminate case: $\\phi=1.001$')\n", - "plt.xlabel(r'Real')\n", - "plt.ylabel(r'Imaginary');" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We clearly see the winding number of -1 here, corresponding to a single clockwise trajectory around the origin.\n", - "\n", - "**Determinate case.** Now let's try the same for $\\phi=1.007$." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYoAAAEYCAYAAABC0LFYAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdd3xb5b348c+jYdmSvFcSJ7bjxNlkgDMIkLADhTI6KIULXDpouZ20pXvcjttS+msLvXRAgZZLyygUKDtAgRAISUjIINsZjme8h2Rbssbz++Mosux4KIllKfb3/Xqd15nSeeJI+p5nK601QgghxGBM8U6AEEKIxCaBQgghxJAkUAghhBiSBAohhBBDkkAhhBBiSBIohBBCDEkChRBCiCFJoBBCCDEkCRRiTFFK7VRKnRvvdAgxlkigEINSSlUopbqVUi6lVJtSap1S6vNKqag+N6HXXxjrdEbSWs/VWr95su8Tj7SPBqXUH5VSPxjB9/uiUmqTUsqrlPprFNdnKaWeVkp1KqUOK6Wui+Zc6Ly73xJQSv3vSP1bxOAs8U6ASHgf1lq/ppRKB1YCdwNLgZtjeVOllEVr7Y/lPcaps4CvjOD71QI/A1YBKVFc/3ugB8gHFgIvKKW2aa13DnMOrbXz6JsopRxAPfDECP5bxGC01rLIMuACVAAX9ju2BAgC80L7k4B/Ao3AIeDLoeMPh67rBtzAN4e6PuJ+3wK2A16MB5kK4PbQsU7gAYwfkpcAF/AakDlQmkPb3wi9th14HEiOuPbbwIHQ++wCrj7RtA/wt5sCPBW6thm4Z7j7hs59C6gJndsLXHC89x4gLSbgm6G/h8b4gf3aCH9Wfgb8dZhrHBiBYEbEsYeBO4Y6N8h73QQcBFS8vyfjYYl7AmRJ3GWgQBE6XgncGvoB2gz8EEgCSkJf3lUDvT7K67eGfmRTIo6tDwWHAqABeB9YBNiA14EfDZTm0PbG0I9sFrAb+HzEtR8PnTMBn8AIRBNPJO39/j5mYBvw29APYDJw9nD3BWYCVcCk0HXFwLRo7g38AfjDIP+P3wPWAFdgBJ8lQACYMsC1zwNtgyzPD/FZiSZQLAK6+x37BvDcUOcGea/Xgf+O93dkvCxSRyFORC3GD+9iIFdr/ROtdY/W+iDwZ+DaQV4XzfW/01pXaa27I479r9a6XmtdA6wFNmitt2itvcDTGD8yg/md1rpWa92C8YO08OgJrfUToXNBrfXjQDnGj+iJpv2oJRiB4HatdafW2qO1fjuK+wYwgt8cpZRVa12htT4Qzb211v+ltf6v/glRSqUC38V4Ap8CbNFab8QISLP6X6+1vlxrnTHIcvkgf5toOTFydpHagdRhzvX/NxViFIM+dJLpEVGSOgpxIgqAFqAImKSUaos4Z8b4MR9INNdXDfC6+ojt7gH2nQzuSMR2F8YPOABKqRuBr2E8uRN6n5yTSPtRU4DDepA6lsHuq7Xer5T6KvDfwFyl1OrQdcf7d450PrBPa12hlFoAbAk1RsjEyJ2NJjeQ1u9YGkYx21Dn+rsReFtrfWjEUygGJIFCHBel1GKMQPE2xg/cIa116SCX95/spGqY6wd6TUwopYownsovAN7VWgeUUlsBNUg6okl75LWFA1XID3dfrfUjwCNKqTTgXuCXGMVK0d67vwkYQR2M3NSTwAqMp/Xt/S9WSr0EnDPIe63VWl96Amk4ah9gUUqVaq3LQ8cWADuHOdffjRj1GmKUSNGTiIpSKk0pdTnwGPA3rfUHGOX/HUqpbymlUpRSZqXUvFAwAePJvyTibYa7fjQ5MIJBI4BS6mZgXsT5k0n7RqAOuEMp5VBKJSulzhruvkqpmUqp85VSNsCDkVsKHOe9+9sDnKGUKgndpwW4B6OC/pigrLW+VGvtHGQ5JkgopSxKqWSMHI459G8d8AFUa92JUcH/k9Df5SzgSuDhoc71u99yjAcVae00iiRQiOE8p5RyYTwlfw/4DaGmsVrrAPBhjCfVQ0ATcD+QHnrtL4Dvh/pgfCOK60eN1noX8GvgXYygcBrwTsQlJ5z2iGunY1T8V2NUWg93XxvGk3ITRpFZHvDdaO6tlPqTUupPA/xT38JoKbYZo1L978AvtdaPRfmnGs73MQLat4H/CG1/PyJdLymlvhtx/X9hNKNtAB4FbtWh5q/DnDvqJuAprfVARVIiRtQADxVCiDFGKXUt8J9a60vinRZx6pEchRDjwwyMprFCHDcJFEKMDzMxKoyFOG5S9CSEEGJIkqMQQggxpDHXjyInJ0cXFxfHOxlCCHFK2bx5c5PWOnegc2MuUBQXF7Np06Z4J0MIIU4pSqnDg52ToichhBBDkkAhhBBiSBIohBBCDEkChRBCiCFJoBBCCDEkCRRCCCGGNOaax55qtNa4vH7au3x4/QG8/qCx+IIAWMwKs0lhMRnrZKuZ1GQLqTYryVYTSqlh7iCEECdHAsUoaHJ72VnbwaFGN5Ut3VS2dFLd2k1LZw+tXT34Aic2jIrFpIygkWwlx5lEXmoyeWk2cp028tJs5KUmMykjhcIsOylJ5hH+VwkhxgsJFCNMa015g5u3y5tYf7CZD2raqWv3xORe/qCmtctHa5ePypauIa/NTbVRlGWnMMvOlCw7Rdl2SnKdlOY5cdjkYyCEGJz8QowArTXbqtt5blstL35Qd9yBIcVqJsNuJcVqJsliwmY1YzObUAoCQY0/qAkENb5AEI8vgMvjx+Xx0xMIRn2PRpeXRpeXTYdbjzk3JSuFGXmpzJiQyox8JzPyU5mW6yTZKrkQIYQEipPi8QV4eksND62rYM+RwSfcSraamD0xjZn5qRRmh57qM+3kptrItCedcLGQ128EjfZuH00uLw3hxUNjh5d6l4fq1m5qWrvxBwcv3qpq6aaqpZt/72kIHzObFKV5ThZMzmD+lHQWTM5g5oRUrGZp/yDEeDPmhhkvKyvTsR7ryR8I8th7Vdzz+n6OdBybe0hLtnDW9ByWT89hSXEW03IdWOL4A+sPBKlr91DZ0hVeKpo6KW9wc6ipk8AQQSRSksUIeAsmpzN/cgZnFGVSnG2XCnUhxgCl1GatddlA5yRHcZy2VrXxnac+YHddR5/jKVYzl82fyIcXTGL5tOyEevK2mE1MCdVNnNXvnNcf4FBTJ/vq3ew74mJvvYvyeheHW7ro/wzR4w+yraqNbVVtgDF+WG6qjSVTs1g6NYslU7OYkZeKySSBQ4ixRHIUUQoGNX9cc4DfvLqvzxN4bqqNW84p4ZqyKaTbrSN+33hxeXzsqOlge3Ub26vb2V7TRlVL97CvS0+xsrg4kyVTs1gyNZt5k9LimpsSQkRnqByFBIooeHwBvv6PbbzwQV34WLLVxJfOL+VTZ00dN01PWzp7woFja1Ub71W04PL4h3xNWrKFs0tzWFGay4oZuUzKSBml1AohjocEipPgCwT5zEObWLOvMXzsjKJM7vrEQqZk2UfsPqeiQFCz94iLjYea2VjRwsZDLTS5e4Z8TWmekxUzclk5I5clU7OkZZUQCUICxQkKBjXfeGIbT22pCR+76cwivn/5nISqg0gUWmsONnWy8ZARNNYdaKK+wzvo9TaLiaUl2Zw7I5eL5uSP+8ArRDxJoDhBv/t3Ob95dV94/4vnTefrF8+QVj5R0lqzr97NW/saeau8kQ2HWujxD973Y+6kNFbNncCquROYke+Uv7MQo0gCxQnYUdPOlb9/J1xx/cklU/j51afJj9dJ6O4JsP5QM2/ta2TNvkYONnYOem1xtt0IGvMmsHByhrSkEiLGJFAcJ38gyOX/+3a4E93i4kwe/ewyab0zwqpbu1izr5FXd9Wzbn/zoD3N81JtXDw3n0vmTmRZSZb8PwgRAxIojtMTm6q4/cntgNG66eWvrKA4xzESyRODcHl8vLG3kdU7j/DmngY6ewIDXpfjtHHFgklcvaiAeQVpksMTYoRIoDgO/kCQC36zhsPNxiB7X7toBl++oHSkkiei4PEFeGd/E6t3HuG13Q20dA7ckmparoOrFxVw5cICqQgX4iRJoDgOL++o4/N/ex8wOo+9/a3zSE0eOx3pTjX+QJD3KlpZvfMIL3xQR6Nr4FZUi4szuWpRAZedNpEMe9Iop1KIU58EiuPwmYfe47XdxuB4XzhvGrevmjVSSRMnyR8Isu5AM89sqeHlnUfoGqB4ympWnDczj4+cPpkLZudJM2YhoiRjPUWpvcsoJz/q42dMiWNqRH8Ws4kVM4we3j/r8fPqrnqe3lLD2vKmcOs0X0Dzyq56XtlVT16qjWsXT+HaJYXSI1yIkxDXQKGUehC4HGjQWs8b4LwC7gY+BHQB/6m1fj9W6Xl7f+8PzvzJ6VKBncDsSRauXGjUTzS6vDy/vZZnttSwrbo9fE2Dy8vvXt/PPW/s5/xZeVy/rIgVpbmYpamtEMcl3jmKvwL3AP83yPlLgdLQshT4Y2gdE2/v781NnDszL1a3ESMsN9XGzWdN5eazpnKg0c1T71fz+HvVNLmN+oyghtd2N/Da7gYmZ6bwySWFXFM2hdxUW5xTLsSpIa4FuFrrt4CWIS65Evg/bVgPZCilJsYqPVurep9Gl5Vkxeo2Ioam5Tq5fdUs3v3O+fzh+tM5a3p2n/PVrd38avVezvzFv/nCI++z7kATY62eToiRFu8cxXAKgKqI/erQsbqBLz9xHl+A8vreWermFaSP9C3EKLKaTXzotIl86LSJHGx08+jGSp7YXE1blw8w5ht/YXsdL2yvY3qek1tWlHDlwknYLDJIoRD9JXqTkIEKk495/FNK3aKU2qSU2tTY2DjAS4a3r94Vni50ao6DNGkSO2aU5Dr53mVzWP+dC/jtJxZQVpTZ5/z+BjfffHI7K+58gz+tOUCHxxenlAqRmBI9UFQDkU2PJgO1/S/SWt+ntS7TWpfl5uae0I0qW7rC29PznCf0HiKxJVvNXL1oMk/eupyXv3oON55ZhCNiLpH6Di93vLSH5b94nV+8uJsj7cdOcyvEeJTogeJZ4EZlWAa0a61HvNgJjLLroyZnSlPKsW7WhDR+cuU81n3nAr51yaw+Fdtur5973zrIOXe+zu1PbOtTJCnEeBTv5rGPAucCOUqpauBHgBVAa/0n4EWMprH7MZrH3hyrtNS19QaKAmlzP26kp1i59dxpfOrsYp7ZUsN9bx3kQGhUW19A88Tmap7YXM2Fs/P43MpplBVlyvhSYtyJa6DQWn9ymPMa+MJopKW9u7dcOlOGgBh3bBYzn1hcyMfPmMK/9zRw75oDbDrcGj5/tHltWVEmX794JmdOyx7i3YQYWxK96GnUdETM/ZyWIhXZ45XJpLhoTj5P3rqcf956JhfPye9zftPhVj755/Xc8MAGtlW1xSmVQoyuRG8eO2o6InIUacnyZxFwRlEW992Yxf4GN/evPchT79eE58xYW97E2vImLpk7ga9fPIPS/NQ4p1aI2JEcRYg3YorOZKu0pRe9puc5ueOj83nj9nO5pmwykSOAvLzzCBff9RZf+8dWqiJazgkxlkigCAlG9M6VsYDEQAoyUrjzYwt45baVXHZa7wABWsNT79dw/q/f5AfP7KChQ5rVirFFAkVIMKIbnzRqEUOZnufk99efzvNfOptzZ/b22/EFNA+vP8yKX73BHS/toa1r4AmXhDjVSKAIiRzvRw3YIVyIvuYVpPPXm5fwj8+dyeLi3t7eHl+QP605wIo73+Dh9YfDIxILcaqSQBESOcGNLxAc4koh+loyNYt/fO5M/nLzYuZMTAsf7/D4+cEzO7jq9++wVVpIiVOYBIqQlIihHAaaOU2IoShlzKz3/JfO5vfXnU5hxBzeH9S0c/Uf3uE7T22ndZD5v4VIZBIoQuwRgaLb5x/iSiEGZzIpLps/kVduW8FtF87AZjG+YlrDoxurOP/Xb/LYxkqCUhwlTiESKELskqMQIyjZauYrF5by6m0rOX9W7yRYrV0+vv3UB3zkj+vYUdM+xDsIkTgkUITYk3o72XV6JUchRkZhtp0H/3Mx999Y1mewya1VbXz4nrf5wTM7aO+SYc1FYpNAEZJp7x22o1nKkcUIu3BOPq/etpIvnz+dJHNvcdTD6w9z/q/f5JktNTLTnkhYEihCcpy9w0w3uSRQiJGXkmTmaxfPZPVtK1gxo7f/RXNnD199fCtfenSL5C5EQpJAEdInULi9cUyJGOum5jh46ObF/Ok/TmdSenL4+PPb67jk7rdYt78pjqkT4lgSKEJyUiVQiNGjlOKSeRN59Wsr+eSS3kkc69o9XP/ABn7+4m68fmlUIRKDBIqQHGfvHBSNLgkUYnQ4bBZ+8ZH53HvDGeF6Mq3hvrcOcuU977D3iMyuJ+JPAkVI5Kx21a3dUrEoRtWquRNY/dUVrIyou9hzxMWH73mbB98+JP0uRFxJoAhJT7GSGpqHotsXoMktFdpidOWlJfPXmxfz4yvmhjvq9fiD/OT5Xdz0l43Uy6i0Ik4kUIQopfoMu1ApcwuIOFBKcdPyYp7/0tl9xo1aW97EqrveYvXOI3FMnRivJFBEiAwUMgmNiKfS/FSe+cJZfG5lSXjY+7YuH597eDN3v1YuRaNiVEmgiBAZKCqaO+OYEiEgyWLiO5fO5pHPLOvTjPa3r+3jK49txeOTVlFidEigiDAtzxne3lcvrU1EYjhzWjYvfWUFZ03PDh97dlst1963ngaX1FuI2JNAEWH2hN4y4T11EihE4ki3W/nrzUu4fmlh+NjWqjauuucddtd1xDFlYjyQQBGhNN/J0emyK5o76ZZRZEUCsZpN/Oyqefzow3PCn9Padg8f/eM6XttVH9/EiTFNAkWEZKuZ4hwHYMyhXd4guQqRWJRS3HzWVB74z8U4bUZz7q6eAJ99eBP3vXVAKrlFTEig6Gd2RJPEbTJ9pUhQ583M46n/Wh4eulxr+PmLe/jWP7fT45epfMXIkkDRT1lRZnj7vYrWOKZEiKHNyE/lX184q89n9h+bqrnhgQ24PDIKrRg5luEvGV8WF2eFtzdVtMQxJeJEaa3x+oN09wTo8gXo7gng8QXw+oP4Ar1Lj1/33Q9ofKFr/EGN1pqgNp7WNTq0Bo4ejzimAItJYTIpLCaF2WTqt9+7WEyKJIsJm8VMsrV3nWw1Y7MY62SLGZvVhM1iQh3tSDGAbKeNv392Kd956gOeer8GgA2HWrj5L+/x0KeW4LDJV1ycPPkU9TNrQiqOJDOdPQFq2z1Ut3YxOdM+/AvFSQsENW6Pnw6Pj/ZuHx0eHy6Pn45uHx2htSt0vqPbR2ePn64eIxB0+wLh7a4eP2NpaCSbxYTTZsGZbMGRZKxTbRYcoWNOm4WiLAc5Tlt45ONNh1s55843ePq/ljMhPRmbxTzMXYQYnASKfixmE6cXZbK23JgT4I29jdywrCjOqTo1eXwBmjt7aHH30NzppaWzp8/S3NlDa8R2e7cUlwzE6w/i9fcc98yLLZ09rPzVmwCkWM1k2q1k2JPIsFvJDK2zHEnkptrIddqMdWiJnBpYCPk0DODC2fnhQPHqrnoJFP30+IM0ur3Ud3ho6PBQ3+GlwWWsjWNe6l0e2uI4W1uS2URKkpkUqxl7ktko1rGasJpNJJlNWM0Kq9mE1dJvP7RtNpkwKTAphVJG0RJKoeh7TCmjJZLWmkAQAkGj2CqgNYGANraP2Q/SEwji9QXx+AN4fEG8/de+AB5/cMQqprt9AbrbjVxyNBxJ5j6BI9dpIy8tmUkZyUxMT6EgI4X8tGSSLFLNOR7ENVAopS4B7gbMwP1a6zv6nf9P4FdATejQPVrr+2Odrovm5POjZ3cC8O6BJtq7fKRHzKk91nV4fNS0dhtLWzfVrV3UtB3d98R0YqfUZAtpycZIvmkpVtKSraSFt3uPpSYbRS/2JDMpSWbsSb3bKVYzVvPY+AELBjUefwC310+nN4Db48ftPbr4cIeP+ej0BnB5/LR3+3ht98n1q+jsCdDZ3EVF8+BjnikFuU4bEzNSKAgFkInpyRRkpDAly05Rtp3U5PHzvRnL4hYolFJm4PfARUA18J5S6lmt9a5+lz6utf7iaKZtUkYK8yens726HV9A88/3q/nU2VNHMwkx5Q8EqWnr5lBTJ4ebu6ho7qSqpYvqUGBwefwjch+zSZHjTCLLYSPLYSXLYSPbkURWv+XosQx7EmbT4BW345HJpEJB0AKp0b9Oa80vX97Ln9YcCB87oyiTH1w+h45uH61dPbR1+Wju7KHR5TUWt5em0HZPYPicjNbQ4PLS4PKyrWrga7IdSRRm2ynKslOY7aAoFEAKs+3kOm1DVtSLxBHPHMUSYL/W+iCAUuox4Eqgf6CIi08snsL26nYA/rb+MDctLz6lfsR8gSDVrd1UNHdyuKmTilBAqGjqpLq1G/9J1PaalDHHeH5aMvlpRpFEXmrEfmoy+WnJZDnkhz9elFJ865KZeP0B/vJOBQCbD7fy6IZKfvGR0zAN8f+itaaj20+j20ODy0uTuydUxOihts1DbXs3tW3dNLi8DNe/rzlU/7Sl8tg+SY4kM9PynEzPdRrr0FKUZccyRnKEY0U8A0UBEPkcUg0sHeC6jyqlVgD7gNu01sc8uyilbgFuASgsLOx/+oRctbCAO17cg8vr52BTJ/98v5pryqYM/8JR5vEFONTUSXmDm/31LmPd4OZQU+cJBwObxURBplEOPTm0NvbtFGSmkJ9qky/yKUApxQ8vn0OPP8jfN1QC8PimKqwWxU+vnDfo07xSinS7lXS7lel5g2djfIFgOHjUtRu50bo2o6Xg4ZYuqlu6h8yZdPYE2F7dHn4gOyrJbKI4x24EjlwnMyekMWdSGkVZ9iEDnIideAaKgf7H+/+yPQc8qrX2KqU+DzwEnH/Mi7S+D7gPoKysbEQaRjpsFj519lTu/nc5AHe+vIdzZ+aSl5o8zCtjo9Pr50Cjm/J6dzgY7G9wUdnSdUJNQSekJVOUbac420FxjoPCLLsRFDJTyHYkSZHAGKGUERS8/iBPbq4G4G/rKynOdvCZc0pO6r2tZhOTM+2DNh8PBDVHOjwcbu6kstkIHpWhnG1lcxcu78BFnD2BIPvq3eyrd/c5bk8yM2tCKnMmpTF7YhpzJqYxa0IaKUnS9DfWVLzGhlFKnQn8t9Z6VWj/OwBa618Mcr0ZaNFapw/1vmVlZXrTpk0jksZOr5/zf/0m9R1G5e0ZRZk89Kkl4TF2YsHl8VHe4Ka83tUnKNS0dR/3e01IS6Y4x87UHAdF2Q6Ks+3hoCDNH8eXQFBz2+NbeXZbLWB0Dnzi82eyqDBzmFfGhtaaJneP8cDT6OZA+OHHzZHjmPLVpGBqjoP5kzNYMDmdhYWZzJ6YKv1GToBSarPWumzAc3EMFBaM4qQLMFo1vQdcp7XeGXHNRK11XWj7auBbWutlQ73vSAYKgLfLm7jhwQ3hsthZE1L5zTULmTMpbegXDqPD46O83sgV7AsFhPJ6F3VRNl88SiljwqXSPCfT81KZnuekNM8o841lQBOnnh5/kI//aR3bQkU9BRkpvPDls8mwJ8U5ZX11eHzhwFHe4GZ3XQe76zqinsfealbMmZjGwikZLAgtU7MdUmw1jIQMFABKqQ8Bd2E0j31Qa/0/SqmfAJu01s8qpX4BXAH4gRbgVq31nqHec6QDBcCDbx/iJ8/3rWO/YFYel8ybwMIpGRRm2/s8wWitcXv9NLl7aHJ7qW3rDmW5u6hsMSqWG13H18TUYlIU5ziYnuukNN8ZCgiplOQ6SLbK05OITlVLF5f9bi0doZZtF87O5883nnFKFDU2uDzsqu1gd52LXXUd7Kpt51BTZ1RFr5l2K4uLs1gy1VjmTEyTerZ+EjZQxEIsAgXAYxsr+dGzO/EO0gEqxWomyWLCHwjiC+iomhcOxGpWlOQYwaA0LzW0dlKU7ZDOTWJErN55hM89vDm8//3LZp90fUW8dPcE2FXXwfbqNrZVtbG1qm3Ivh9HOZLMnFGcxZLiTJZMzWbhlIxx//2SQDFCyutd/PLlvSfdmQmMlh0luQ6m5zmZkZ9KaZ6T0vxUirLtY6azmEhcP3luFw++cwiIf33FSGvt7GF7TTtbK9vYVt3GlspWWocZJcCeZObMkmzOKc3hnBm5lOQ4Tolc1kiSQDHCDjV18tKOOjZVtLKnroMGl/eYpqjJVhM5Tluov4GNomyjErk420FRtp2J6cmS9RVxc6rUV4wErTUHGt1sONTCxtAyXF1gQUYK55TmcHZpDueU5pKeMvZ7mEugiLGjdRK+gA6PGTTc8NBCxNupXF9xMrTWVLd2h4PGuoNNVLUM3qrQYlIsK8nmojn5XDgnn4KMlFFM7eiRQCGEGNBYqq84UVprDjd3sXZ/E2v3NbLuQDPuQfp4AMydlMZFc/JZNXcCsyakjpnAKoFCCDGoHz+3MzzMR5LFxJvfOJdJY/SpORq+QJBtVW28Vd7EG3sa+KCmfdBrS/OcXLFgEh9eMIniHMcopnLkSaAQQgyqxx/k6j+8w87aDgCuKZvMnR9bEOdUJY669m5e21XPK7vqWX+wGV9g4N/MBZPT+fCCSVyxYBJ5afEZweFkSKAQQgxp3f4mrrt/A2D0dl791RWU5h/HcLXjRIfHx5q9jbyyq57XdtXT7Qscc43ZpDhvZh7XLp7CuTNzT5lGKxIohBDDuuGBDeEJu1bNzefeGwb8zRAhXT1+Xt1Vz3Pbalmzr3HAnEZ+mo1ryqZwTdkUpmQl9pTKEiiEEMPaUdPO5f/7dnj/qf9azuljpG9FrLV19fDyjiM8taWGjYdajjlvUnDxnAl8+pyplBVlJmQFuAQKIURUvvjI+zy/vQ6ApVOzeOyWZQn5o5bIDja6eXxTFf/cXD3g+FQLJqfzqbOn8qHTJiZU59qTDhRKKbPW+tjCuAQkgUKIE1fR1MmFv1kT7kD615sXc+7MvDin6tTU4w/y+p56/r6hMlykF2lKVgpfOq+Uq08vSIiAMVSgiDZ1+5VSv1JKzRnBdAkhEkxxjoNrl/RO0PXLl/cSPInZEMezJIuJS+ZN5OFPL+WV21Zw7eIpfcaTqmrp5pv/3M4Fv17DPzZV4TvB8eFGQ7SBYj7GkOD3K6XWK6VuUUqd3DjbQhc/W+wAACAASURBVIiE9OXzS0kJjUi8u66D57bXxjlFp74Z+anc8dH5rPv2+Xz1wlIy7L1DglS2dPHNJ7ez6q63eH1PPYlYHRBVoNBau7TWf9ZaLwe+CfwIqFNKPaSUmh7TFAohRlVeWjKfOrs4vH/3v8sT8sfrVJTjtPHVC2fw9rfO5/ZVM/sEjIONnXzqr5u48cGN7Kt3xTGVx4oqUCilzEqpK5RSTwN3A78GSjCmKn0xhukTQsTBLSumhSe+OtjYye66xPrhOtU5bRa+cN501n7zPL5x8QxSIyYZW1vexKV3r+UXL+3GM0A/jXiItuipHLgS+JXWepHW+jda63qt9ZPAy7FLnhAiHtJTrJw/q7cS++WdR+KYmrErNdnKF88v5Y3bz+W6pYUcnYQvENTcu+YgH7p7Le9VHNvcdrQNGyhCc1X/VWv9aa31uv7ntdZfjknKhBBxdcm8CeHtVyRQxFSO08bPrz6NF758DkunZoWPH2zq5ON/epc7XtqDP46V3cMGilCz2PNGIS1CiASyckYutlArnT1HXBxq6oxzisa+2RPTeOyWZfz86tP6zHn/pzUH+OSf13NkmHk0YiXaoqd1Sql7lFLnKKVOP7rENGVCiLhy2CycU5ob3l8tuYpRoZTiuqWFvPq1FZxTmhM+/l5FK5f9bi1bq9pGPU3RBorlwFzgJxgV2b8G/l+sEiWESAyRxU8v75BAMZompqfw0M1LuH3VzHDdRXNnD9fe9y6v7Tr56ZiPR7TNY88bYDk/1okTQsTXhbPzMId+pbZWtcWt6GO8MpkUXzhvOo98dhmZoaa0Hl+QWx7exLPbRq9/S9T9xpVSlymlvqmU+uHRJZYJE0LEX4Y9iTNLssP7r+ySXEU8LCvJ5p+3LmdKljGhVFDDbY9vHbVGBtH2o/gT8AngS4ACPg4UxTBdQogEsUqKnxJCSa6Tp249i9I8J2A0of3iI1tGpc4i6joKrfWNQKvW+sfAmcCUYV4jhBgDVs3JD2+/V9EiYz/FUW6qjb9/ZilF2cbcFj2BILf+bTNNbm9M7xttoOgOrbuUUpMAHzA1NkkSQiSSvLTk8FATvoCmufPYobPF6MlLS+b/PrWEtGSj+Wxdu4fvPvVBTO8ZbaB4XimVAfwKeB+oAB6LVaKEEIllQsQc0FKhHX9F2Q7uvnZReP+VXfUxLRaMttXTT7XWbVrrf2LUTczSWv8gZqkSQiSU/IhAsbOuPY4pEUedN8uYl/uoO1fvIRCjYsHjafW0XCl1HUal9pVKqRtjkiIhRMKxmntnufvvZ3fyQbUEi0Tw7UtnhQcUPNjYGbNOkdG2enoYo4Pd2cDi0CIzrwsxDry8o4439jaG9z2+INfc+6701E4AGfYkblze2wD1iU1VMbmPZfhLACMozNEyKL0Q44bWmnvfOsBdr5UfU6TR7Qvwlce2cNtFM7jlnBKZVzuOrl1cyO/fOADA2/ubcHl8pCZbh3nV8Ym26GkHMGHYq46TUuoSpdRepdR+pdS3BzhvU0o9Hjq/QSlVPNJpEGK0BQIBnn/+eX7605/y/PPPEwgkxpwDkfyBIN98cjt3v1aOxzfwqKUeX5C7Xi3n2099ENeRTce7KVl2Zk1IBYxWaTtqOkb8HtHmKHKAXUqpjUC4wa7W+ooTvXFo+PLfAxcB1cB7Sqlntda7Ii77NEbfjelKqWuBX2LUkQhxSgoEAqxatYoNGzbQ2dmJw+Fg6dKlrF69GrPZHO/kAeDy+Pj0XzfxQU073YMEiaO6fQGe3VrD4eZO7r9pcZ8RT8XomT85nT1HjMml9tW7OHNa9jCvOD7R/q/+94je1bAE2K+1PgiglHoMY3KkyEBxZcS9nwTuUUopKQITp6qXXnqJDRs24Ha7AXC73WzYsIGXXnqJyy+/PM6pA48vwOW/e5vKli6i/ZJ1+4JsONjC5b9by8tfXUGyNTEC3niSl9rbKq292zfi7x9VoNBarxnxO0MBEFnzUg0sHewarbVfKdUOZANNkRcppW4BbgEoLCyMQVKFGBlbtmyhs7PvvA6dnZ1s3bo1IQKFzWJi1bwJdHr9fY73+IM8sbk6vH9N2WSs5r4l106bJTx/hRhdRwduBPDFoBhwyEChlHpba322UsoFfR4wFKC11mknce+Bar/6P8REcw1a6/uA+wDKysoktyES1qJFi3A4HOEcBYDD4WDhwoVxTFUvpRTf/dDsY46X17v6BIofXD5nxCtMxYmLHMIjy5E04u8/ZPjXWp8dWqdqrdMiltSTDBJg5CAix4uaDPQfNzd8jVLKAqQD8Z9AVogTdOmll7J06VKcTidKKZxOJ0uXLuXSSy+Nd9KGVBfRG3tZSZYEiQSzo7a3AvvoOFAjKaqiJ6VU1gCHXVrrkykMew8oVUpNBWqAa4Hr+l3zLHAT8C7wMeB1qZ8QpzKz2czq1at56aWX2Lp1KwsXLuTSSy9NmIrswRzp6A0UkcN5iPhrcnvZUdPbAXLRlMwRv0e0ldnvYzzZt2IUB2UAdUqpBuCzWuvNx3vjUJ3DF4HVgBl4UGu9Uyn1E2CT1vpZ4AHgYaXUfoycxLXHex8hEo3ZbObyyy9PiDqJaNVH5Cjy0yVQJJJnttSE+7mUFWWSGYOip2gDxcvA01rr1QBKqYuBS4B/AH/g2EroqGitXwRe7HfshxHbHoy5L4QQcSQ5isTk8QX489qD4f2PnD45JveJtolC2dEgAaC1fgVYobVeD9hikjIhRMKIHDF2ouQoEsYf3zxAfYdRkZ2XauMjpxfE5D7R5ihalFLfondo8U8AraFOc9IlU4gxLjJHkS85ioSws7adP7y5P7z/pQtKY9aHJdocxXUYrZKeAf4FFIaOmYFrYpIyIUTCqI8sepIcRdy1dPbwuYc34wsYdRNnFGVy/ZLY9SGLtsNdE8Z82QPZP8hxIcQY0N7to8ltzGpnUpDrlNLmeOrq8fP5hzdT3WpMPOpIMnPnx+ZjMsVuYMZom8fmAt8E5gLhxwmt9fkxSpcQIkG8vqc+vH1aQToWs/S+jhePL8Bn/28TGyt6u5Pdde0ipuU6Y3rfaP/H/w7swZgn+8cYU6G+F6M0CSESSOQUm6vmjfgg0iJKbV093PjARt7Z3xw+9r0PzeaiOfkxv3e0gSJba/0A4NNar9FafwpYFsN0CSESQFePnzX7eictumSuBIp4ONzcyUf+uK5PTuL2VTP57IqSUbl/tK2ejvbArlNKXYYx1EZsGuwKIRLGW/saw/NRlOY5KYlxEYc41ss76rj9ye24PL0DNX73Q7O4ZcW0UUtDtIHiZ0qpdODrwP8CacBtMUuVECIhrN7ZWz9xiRQ7jarungC/fHkPf11XET6WZDHx22sWctn8iaOalmhbPT0f2mwHzotdcoQQiaLHH+S13b2BYpUUO42a9Qeb+fY/t1PR3BU+NjkzhXuuO52FUzJGPT3RtnqaitE8tjjyNSczw50QIrG9e7A5XNwxOTOFuZNOdsBoMZzWzh5+9cpeHtlQ2ef4xXPy+dXHFpBuj8+ovdEWPT2DMUDfc0hPbCHGhT6tneZOQKnYtdMf73yBIA+/e5i7XttHR0RdRKrNwvcum80nFk+J698/2kDh0Vr/LqYpEUIkDK8/wKu7egOF1E/ERjCoWb3zCP/vlb0caOw78+GFs/P52VXzEqInfLSB4m6l1I+AV4DwVEpa6/djkiohRFz9fX1luDd2XqqN0wtHfo6D8UxrzSu76rnrtXJ213X0OVeUbQ/3j0iUXFy0geI04AbgfHqLnnRoXwgxhrg8Pu55o3dkns+vnNZnTmZx4nyBIC9+UMd9bx1kZ23fAJFqs/ClC6Zz0/JibJbEmsgq2kBxNVCite6JZWKEEPH357WHaOk0vuoFGSlcvyx2g82NFy6Pj8c2VvGXdw5RGzFkO0CK1cxNy4u5ZUVJTOa7HgnRBoptGLPaNcQwLUKIOGtye7k/YiKcr100I+Gebk8le4+4eHRjJf/cXI3L6+9zzmYxceOZRXxu5TRyEnygxWgDRT6wRyn1Hn3rKKR5rBBjyD2v76erJwDAzPxUrloUm4lwxjKPL8Dz2+t4dGMlmw+3HnM+y5HEDcuKuOHMooQPEEdFGyh+FNNUCCHirqqli79vOBzev33VTKmbiFIwqHmvooVnttbywvbaPk1cj5qW6+Az55Rw9aKCmE0wFCvR9sxeE+uECCHi6zev7gtPhFNWlMkFs/PinKLEt7uug39treW5bbXUtHUfc95qVlw8dwLXLSnkzJLsmM4ZEUtDBgqllAujddMxpwCttZaumkKMAbvrOnhma014/1uXzkqYppmJRGvNztoOVu88wuqdR9hX7x7wuqJsO59cUsjHzph8yhQvDWXIQKG1Th2thAgh4kNrzc9f3I0OPRJeMCuPxcVZ8U1UAgkENZsqWli9s57VO48MmHMAyLRbuWz+RK5cWMAZhZmnbO5hINHWUQghxqgH3j7E2vImAJSC2y+ZGecUxV+jy8tb+xpZs6+RteWNtHb5Brwu2WriojkTuGrhJM4pzSXJMjZn/5NAIcQ4tqWylTte2hPev3n5VGZNGH8lyr5AkC2Vbby5t4E1+xqP6QwXKTXZwgWz8lg1dwIrZ+ZiTxr7P6Nj/18ohBhQW1cPX3xkC/6gUea0YHI63750VpxTNTp8gSDbq9tZf7CZ9Qeb2Xy4NdwseCC5qTYunpPPqrkTWFaSPWZzDoORQHECunsCvL2/iQ0HmylvcFPX3k1bl4+gNsp7HTYLmXYr6fYkchxJlOQ6mJ7nZHqek8Isx7j7kInEo7XmG09sD5e3pyZbuOe608fsZ9PjC7Cztp31B1uiCgwWk+L0okxWzsjl3Jm5zJ6QNqbqHI6XBIrj0N7t4/dv7OexjZUDtpM+qrmzh8qWgc9ZTIrCbDvTco3AMT3XybQ8J9NyHaQmx2eseTH+PPhORZ9JiX71sQVMybLHMUUjR2tNTVs371e2saWylfcr29hV2x5u+juYgowUVszIZeWMXJZPzyZNvo9hEiii9O6BZr706Baa3N7hLx6CP6g52NjJwcZOXt1V3+dcfpqN6XnOY4JIXqpNmiqKEbO1qo07Xtod3r/5rOJTehjxZreXnbUd7KhtZ1tVG+9XttHoGv57WpCRwrKSbJaVZLGsJHvMBMpYkEARhVd31XPr3zaHy3IBirPtrJo7gUWFGUzOtJPtTMKsFChwe/y0dfto6+qhts3DgUY3Bxo7OdDgHrRpHUB9h5f6Di/v7G/uczzVZgnlOpyhQOIIFWPZsZjHZlGBiI32Lh9f+Pv74afrBZPT+c6ls+OcquhorTnS4WFHTQc7atrZWdvBztp26voNsjeYqTkOTi/M5Mxp2SydmiWB4ThIoBjGztp2vvTo++EgkeO08eMr5nLpvAmDllnmDdH7pNPr51BTJ/sb3BxodLO/wVgqmjsHzRq7vH62VrWxtaqtz3GrWVGc7aAk18G0XCcluU5jO8cZtykTReLSWvONJ7edEvUS7V0+9jW42HvERXm9i331bvbWu8Kj2g7HabOwcEoGiwozOL0wk4VTMshM0JFZTwVxCRRKqSzgcYw5uCuAa7TWx4yepZQKAB+EditHexBCXyDI1/+xDY/PmIKjKNvOo59dxqSMlBN+T4fNwryCdOYVpPc57g8EqWzp4kBjZzh4HGh0c6DBfcyok73p05Q3uClvcAN9i7FynEmU5Bq5j5IcJ9PyjHVBZgpWyYWMS799dV+f4s5410torWl0e6lo6uJAo5t99S7K6411QxRFR0fZLCZmTUxj3qQ05hWkc3phJtPznDJO1QiKV47i28C/tdZ3KKW+Hdr/1gDXdWutF45u0no9sqGSPUdcgNGx5oGbFp9UkBiKxWwK5QicXDQnP3xca02jy2sEj1DgMNadHOkYPMvd5O6hyd3CxkN9a9XNJsWkjGQKs+wUZjlCaztF2XamZNlJT5GcyFj0+zf287vXeycjGq16Ca01rV0+DjV1UtHUyaGmTg41G9sVTZ10DtHyaCBOm4U5k9KYNymduaHAMC3XIUWwMRavQHElcG5o+yHgTQYOFHHj9Qf445sHwvtfvXAG0/Oco54OpRR5acnkpSWzfHpOn3Muj8+oGG8yAsfR9aHmTnr8wQHfLxDUVLV0U9XSzTs0H3M+PcVqBI1MO5MykpmYnhJeT8xIJsdhG9fNBE9F9689yK9W7w3vr5yRO2L1EkcDQXVrFzWt3dS0dVPdaizGdheuIVoIDibJYmJ6rpMZ+U5K81OZkZ/KjHwnUzLt8vmLg3gFinytdR2A1rpOKTXYMJXJSqlNgB+4Q2v9zEAXKaVuAW4BKCwcmdm4Xt5xJPzEnuO08Z/Li0fkfUdSarKVBVMyWDAlo8/xQFBT09rNgSY3Bxs7OdDo5mCjsT1clr6928f26na2V7cPeD7JbCI/3WYEkPRkJmakMDE9mbxUG7mpNnKdyeSm2khJOrWGUR6rHn63gp+90NvCafm0bO694Yyo6iX8gSBN7h7qOzw0uLzGusNDfYeXIx0eatq6qWntptt3fLmCSKk2C8U5DqbmOCjNOxoUnBRlO6ToKIHELFAopV4DBsrbfu843qZQa12rlCoBXldKfaC1PtD/Iq31fcB9AGVlZUM3lo7Sk5urw9s3nVl0So0fbw711SjMtnNev2F7PL4AVS1dVEYuzb3b3kFyIkf1BILhHMlQnDYLOc4kI3ik2sh12sLbmfYkMuxJZNqtZNiTyLBbpd4kBv7xXhU/+NfO8P7i4kzuvnYRjS4vLZ09tHT10OLuobWrh+ZOY7vJ7aXeZQSDJrc3PFDgybAnmSnONoJBcY49YttBtiNJmn6fAmIWKLTWFw52TilVr5SaGMpNTGSQKVa11rWh9UGl1JvAIuCYQDHSGl1e3t7fO0jaR8+YHOtbjppkq5nS/FRK849tmnW0PuRo0Khr91Db1t1n3d498OBo/bm9ftxePxXNXVFd77RZyLBbQ0HE2htIUqw4bBacyRactogl2UKqzYrDZsaZbBkX03VqrfH6g3T3BHB7/XR4fHR0+3F5fHR4Qutu4/jD6w8fU/z4XkUri//ntRFPlyPJzORMOwWZKUzOTKEgIyW0bacgI4UcpwSDU128ip6eBW4C7git/9X/AqVUJtCltfYqpXKAs4A7RyNxb+5tCD9JLS7OilkFdqKJrA8pG2SY6a4eP7VtHurau6lr81Db3s2Rdg9Nbi+N7h6aXF4aXV56AkPnTPo7GliqW4fOqQwmyWzCYTNjT7Jgs5iwWc0kW00kW8zYQutkq4lkqxmbpXdtMZswmxQWk8JiNmExqfC+2aSwmBVmk3HcpEBrY4IWY218SHqPRe5rAkGj5ZyxaGPtD+ILRmwHjP0ef5BuX4DuHmPp8gXo7vH3OdbtCxAckfxy9LIdSeSlJZOfZiM/NZm8NJvxGUm1UZBhBIb0FKsEgjEuXoHiDuAfSqlPA5XAxwGUUmXA57XWnwFmA/cqpYKACaOOYtdoJO6Nvb0ZnPNnySxfkexJlvC4VYPRWtPR7afRbZRtN7l7aAwFkEaXl7Yuo7jD6JRodEw82R/AnkCQnq7goMNBC0OSxUS2I4msiCXTnkS2I4lMRxI5ziTy05LJT0smx2lLyD4WYvTFJVBorZuBCwY4vgn4TGh7HXDaKCeNYFCHx+YHCRQnQilFut1Kut3K9KF6H4YEgxqXx09bdw+tXT4jiHT10Nblo73bR2cot+Hy+Ptsu73Gvsvj79NrfixLsphIsZpxJJlJS7GSlmwlLcVCarKVdQeaqO/o21jht59YQEmOMxwU7ElmefoXx016ZvdT3uAON+fLcdoojUOT2PHGZOoNLEXZx//6o2X3bq+f7p4AXn8Qjy+A1x/A4zu6baz77weCGn9QEwgGQ+vQfuDY44GgxhilRRnr0DYAxugtKKVCa6NRgdVkwmpRWM0mkswmrGYTFnPkvlHklWQ2kZJkJsVqxp5kJjnJWKdYzeHjKVbzgP0FtNb84c0DPL2ldyrTomw7j99yJhPSk0/sP0WICBIo+nm/sreD+BlFGfL0dQpQSpFsNZ9SLdNGitcf4DtPfcBT7/cNEn//zFIJEmLESKDoZ/PhyECRGceUCDG0ZreXzz28mU0Rn9mlU7P403+cIeMaiRElgaKfHTW9Hc0WFUqgEIlp7xEXn37ovT6txD5RNoWfXjVPKqDFiJNAEcEXCHKg0R3enzVh+IpYIUbbG3sa+NKjW3CHBotUCr576Ww+c85UKSoVMSGBIsLBxt6hvgsyUmTGOZFQtNY8+E4F//PCrnBzYkeSmbuvXcSFEQNJCjHSJFBE2FvvCm/PlNyESCC+QJAf/msnj26sDB8ryEjh/pvKmD0xLY4pE+OBBIoI+470BooZAwxxIUQ8HG7u5KuPb2VLZe/EVYsKM7jvhjJyU21xTJkYLyRQRKho7gxvT8t1xDElQhhFTU9srubHz+7sM2/DFQsmcefH5o/L5sAiPiRQRKiKaEFSKPPpijhq7ezhO099wMs7j4SPWUyKr108g1tXTpNKazGqJFBEqGrpHelUJl4X8fLWvka+8cS2PnOHlOQ4uOvahcyfnDHEK4WIDQkUIZ1ef3jidqtZkZ8mvVrF6PL4Avzy5T385Z2KPsevX1rI9y6bjT1Jvq4iPuSTFxLZcakgI0Vm1xKjanddB195bAv76nv78WQ7krjzY/O5YLY0fRXxJYEipD407SnAxPTxMf+EiL9gUPPgO4e48+W9febwOH9WHr/86Hxp1SQSggSKkKPFTgDZThknR8Te7roOfvivHbxX0TtWU7LVxPcvm8P1SwulwlokDAkUIU3u3orDbBlQTcSQy+Pjt6+W89C7FQQi5tE4rSCdu65dyLRcGdpeJBYJFCF9cxSS3RcjT2vNs9tq+dkLu2mMaNFkMSluPXcaX76gFOsA800IEW8SKEKa3b2BIktyFGKE7at38YNndrDhUEuf48unZfOTK+dGNROgEPEigSKkw9M713KGXQYDFCPD7fXzu3+X8+Dbh/pM15qfZuP7l83h8vkTpS5CJDwJFCEeX+8QCSkyNII4SVprXvigjp89v5sjES3qzCbFp84q5isXzsBpk6+fODXIJzXE4+ttmihj6IiTsau2g5+/uJu39zf1Ob5kahY/vXKejEwsTjkSKEI8/t4cRbJVKhTF8TvY6Oa3r5Xz3LbaPsdznDa+d9ksrlpYIMVM4pQkgSIkMkdhs0iOQkSvtq2b3/27nCc2V/dp7mpScOOZxdx20QzSU6TeS5y6JFCEBCO+4DJ8h4hGs9vLH948wMPrD9PjD/Y5d9GcfL5+8QxmTZBJhcSpTwJFiMXcGxwinwqF6K/D4+P+tYd4YO3BPvNEgNHc9fZVM1lUmBmn1Akx8iRQhFgiOjr5AsEhrhTjlccX4KF1FfxxzQHaunx9zi2YksE3V83krOk5cUqdELEjgSLEElHc5JcchYjg8vh4bGMVf157sM8cEQAz8p184+KZXDQnXyqqxZglgSLEntRbge32+uOYEpEoGjo8/GVdBX9bfxiXp+9nojDLzm0XlXLFggKp0xJjngSKkMhhO1ojxn0S48/+Bjd/fusgT2+p6TP0N0Beqo0vX1DKNWVTSLJIM2oxPkigCMm09waKFgkU49Kmihbufesgr+6qP+ZcSY6DW1aUcNWiAumQKcaduAQKpdTHgf8GZgNLtNabBrnuEuBuwAzcr7W+I1ZpisxRNPYrhxZjVzCoeW13Pfe+dZDNh1uPOb+oMIPPr5zGRbPzMUkRkxin4pWj2AF8BLh3sAuUUmbg98BFQDXwnlLqWa31rlgkaEpW76x2h5u7YnELkUA6PD6efr+G/3u3ggONncecv3B2Hp9bOY2yokyppBbjXlwChdZ6NzDcF3AJsF9rfTB07WPAlUBMAsXUnN7JYg41HfvDIcaG7dVt/H19Jc9uq6Xb17cPhNWsuHpRAZ89p4TSfBmPSYijErmOogCoitivBpbG6mZTcxzh7YNNbjy+gJRFjxFdPX6e3VrL3zdU8kFN+zHnU20WrltWyM3LpzIhPTkOKRQiscUsUCilXgMmDHDqe1rrf0XzFgMcG7CDg1LqFuAWgMLCwqjTGCk9xUpJjoODTZ34ApoPatpZXJx1Qu8lEsOeIx08sqGSp9+vwTVAk+dZE1K5flkRVy2cRGqyjMUkxGBiFii01hee5FtUA1Mi9icDtQNdqLW+D7gPoKys7IR7y51RlMnBULHTpopWCRSnoK4ePy/vOMIjGyrZNEDldJLFxOXzJ3L90iJOL8yQ+gchopDIRU/vAaVKqalADXAtcF0sb7i4OIsnNlcDsGZfA7eeOy2WtxMjJBDUvLO/iWe21PDyziN09Rt/CYzmrdctLeSjp08mU6a6FeK4xKt57NXA/wK5wAtKqa1a61VKqUkYzWA/pLX2K6W+CKzGaB77oNZ6ZyzTde7MXJQCrWHjoRaa3F5ynLZY3lKcIK01O2s7eHpLDc9uqx2wSbPFpFg1dwLXLy3kzGnZknsQ4gTFq9XT08DTAxyvBT4Usf8i8OJopSsvLZnFRVlsrGghqOGfm6v53ErJVSSSqpYunt1Wy9Nbatjf4B7wmpJcBx89fTIfL5tMXqpUTgtxshK56CkuPlY2mY0VLQA8tK6CT589tc/IsmL0NXR4WL2rnue21ob/b/rLcdq4YsEkrl5UwLyCNMk9CDGCJFD0c8WCSfzypT00d/ZQ2+7hsfeq+I9lRfFO1rhT0dTJ6p1HWL3zCFuq2tADNFFIsZq5ZN4ErlpUwFnTsiWgCxEjEij6Sbaa+dTZU/nV6r0A/PqVvXx4/iTS7dJ8Mpa01uyq62D1znpW7zjC3nrXgNeZFJxTmsvViwq4aE4+Dpt8hIWINfmWDeDTZ0/l0Y2VVLd209rl49tPbecP158uxRkjzBcIsvlwK6/uqmf1ziNUt3YPeJ3ZpFhSnMWqmDNOqgAACYZJREFUufl8aP5EqXcQYpRJoBhAstXMDy6fw+ce3gzASzuOcP/aQ3x2RUmcU3bqq2rpYs2+Rtbsa+TdA82Dzv2RZDGxojSHVXMncOHsfGnSKkQcSaAYxKq5E7jpzCIeevcwAL94aTf56clcsWBSnFN2aunq8bP+YDNv7Wtizb7GIcfRSrVZOH92HqvmTmDljFwpVhIiQcg3cQjfu2wO22va2VLZRlDDVx/bQntXDzecWRzvpCUsjy/A9up2Nh5qZt2BZjZVtB4z+U+kgowUVs7M5eI5+SyfliOTAQmRgCRQDCHJYuLPN5bxyfvWU97gJqjhB//aydaqdn5y5Vx54gU6vX7er2xl46EWNhxqYWtVGz3+wQNDstXEspJsVs7IZcWMXEpyHFL3I0SCU3qgdoensLKyMr1p04DzIJ2wJreXT//1PbZV9448Oik9mR9+eC6r5uaPmx86rTXVrd1sr25na1UrGyta2VHTTiA49GdoZn4qK2bksHJGHmXFmTIqrxAJSCm1WWtdNuA5CRTR6erx8/1ndvDU+zV9ji+cksFXLihl5YzcMTcDWoPLw/aqdrZXt7G9pp3t1e1RTRNbkuNgcXEWi6dmcdb0bCampwz7GiFEfEmgGEH/2lrDj5/bdcwPZnG2nWuXFHLZaROZkmWP2f1jweMLcKDRzb56F3uPuCmvd7GrroO6ds+wr1XKyDEsnZrFkqnZLJ6aKc1XhTgFSaAYYW1dPdz1WjmPbKgcsKJ2/uR0zinN4azpOZxemBhFLcGgpsHlpbKli8qWLg43d1JebwSHiuZOhik9CktNtjB/cjrzJ2dQVpRJWVGWdEYUYgyQQBEjtW3dPPD2IZ7YVEWHZ+D+AGaTojTPybyCdGbmp1KYbacwy1hGqjI8GNS0dPXQ0OGlweWhweWl0eWlocNDVWs3lS1dVLV04R2iknkgyVYT8yYZQWHBFGNdlGUfc0VsQggJFDHX1ePnxQ+O8Pz2Wt4ub8If5eN5itVMliOJTIeVTHsSNosZm9WEzWIsoAgEg/iDmmBQ4w9qunsCuDx+Ojw+3F4/Lo8fl8cXdY5gIEpBUZadGfmpxjIhlZn5qUzLdcj4SUKMExIoRlF7l491B5p450AT6w40c7Bx8A5moy3TbjVyM9kOpvz/9u4uRq6yjuP499fudnfJ7gJ9WVqgtA00IgKuadPIBWBCIfSmeEEQI7EEEmPQcEEgIYEb9ULFGLzQC41cIDcqJALKO1W5aglokYAEW99CSyOCpaEvLEv79+I8kHEzfebYPXPOzOzvk0x2zsyT7f+fSfd35rw8z+ljnLtinE+snODcFeOMLWn+8JiZNScXFL4RoGKnnjLMlotWseWiVQC89/4sr+1/j1f2HeTvbx/++BzBvgNHszei/d//7tgwUxMjTE2OsGJ8hKnJUaYmRjjztLEUDqcw6XWhzewkOCi6bGJ0mE3rlrJp3f+uvx0RHJr5kAOHZ3nn8AzvHp1lZvY4Hxw7zszssY/PJwwtEosXiaHFYpHE2PBiJkaHmRgdSo9hxkeGfEezmXWNg6IhktIf/GHOWdZfl9Oa2cLi3VAzM8tyUJiZWZaDwszMshwUZmaW5aAwM7MsB4WZmWU5KMzMLGvgpvCQ9G/gn03X0QXLgbebLqImC6VX9zl4+rnXNRGxot0bAxcUg0rSiyeah2XQLJRe3efgGdRefejJzMyyHBRmZpbloOgfP2m6gBotlF7d5+AZyF59jsLMzLL8jcLMzLIcFGZmluWg6FGSlkp6RtLu9PP0zNhJSfsk/bDOGqtSpldJ05J2SHpV0suSvtBErSdD0tWSXpe0R9Kdbd4fkfSL9P7zktbWX+X8lejzNkl/Tp/fdklrmqizCp16bRl3raSQ1NeXzDooetedwPaIWA9sT9sn8i3guVqq6o4yvR4BvhwRnwKuBn4g6bQaazwpkhYDPwK2ABcAX5R0wZxhNwMHIuI84F7gu/VWOX8l+9wFbIyIi4GHgHvqrbIaJXtF0gRwK/B8vRVWz0HRu64B7k/P7wc+326QpA3AGcDTNdXVDR17jYi/RMTu9PxN4C2g7V2kPWYTsCci/hYRHwA/p+i3VWv/DwFXSFKNNVahY58R8buIOJI2dwJn11xjVcp8plDswN0DvF9ncd3goOhdZ0TEfoD0c2ruAEmLgO8Dd9RcW9U69tpK0iZgCfDXGmqbr7OAN1q296bX2o6JiA+Bg8CyWqqrTpk+W90MPNHVirqnY6+SPgOsjojf1FlYt3jN7AZJehZY2eatu0r+iluAxyPijV7fAa2g149+zyrgAWBbRByvorYua/fBzL0mvcyYXle6B0k3ABuBy7taUfdke007cPcCN9ZVULc5KBoUEZtP9J6kf0laFRH70x/Ht9oMuwS4VNItwDiwRNKhiMidz2hEBb0iaRJ4DLg7InZ2qdSq7QVWt2yfDbx5gjF7JQ0BpwL/qae8ypTpE0mbKXYOLo+ImZpqq1qnXieAC4Hfpx24lcCjkrZGxIu1VVkhH3rqXY8C29LzbcAjcwdExJci4pyIWAvcDvysF0OihI69SloC/IqixwdrrG2+XgDWS1qXerieot9Wrf1fC/w2+u9O2I59psMxPwa2RkTbnYE+ke01Ig5GxPKIWJv+b+6k6LkvQwIcFL3sO8CVknYDV6ZtJG2U9NNGK6temV6vAy4DbpT0UnpMN1Nueemcw9eBp4DXgF9GxKuSvilpaxp2H7BM0h7gNvJXuPWkkn1+j+Kb74Pp85sbmH2hZK8DxVN4mJlZlr9RmJlZloPCzMyyHBRmZpbloDAzsywHhZmZZTkozCog6Vi65PMVSb+ez4SFkv4haXmV9ZnNh4PCrBpHI2I6Ii6kuKv6a00XZFYVB4VZ9XbQMkmcpDskvZDWYfhGy+sPS/pDWmPjK41UalaCg8KsQmmtgitIUzpIugpYTzE19TSwQdJlafhNEbGBYoK8WyX124yxtkA4KMyqMSbpJeAdYCnwTHr9qvTYBfwROJ8iOKAIhz9RzAW0uuV1s57ioDCrxtGImAbWUKyV8dE5CgHfTucvpiPivIi4T9LngM3AJRHxaYogGW2icLNOHBRmFYqIgxTLX94uaZhi4ribJI0DSDpL0hTFVOIHIuKIpPOBzzZWtFkHXo/CrGIRsSsdUro+Ih6Q9ElgR1qb4BBwA/Ak8FVJLwOvUxx+MutJnj3WzMyyfOjJzMyyHBRmZpbloDAzsywHhZmZZTkozMwsy0FhZmZZDgozM8v6L5Ki8Pud4vaaAAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "phi = 1.007\n", - "ss_cur = {**ss, 'phi': phi}\n", - "A_cur = sj.get_H_U(block_list, unknowns, targets, T, ss_cur,\n", - " asymptotic=True, use_saved=True)\n", - "\n", - "det_Alambda = sj.determinacy.detA_path(A_cur)\n", - "x, y = det_Alambda.real, det_Alambda.imag\n", - "\n", - "# plot curve\n", - "plt.plot(x, y, label=r'$\\det A(\\lambda)$', linewidth=3);\n", - "\n", - "# dot for origin\n", - "plt.plot(0, 0, marker='o', markersize=5, color=\"black\")\n", - "\n", - "# arrow to show orientation (using rate of change around lambda=0)\n", - "plt.arrow(x[0], y[0], 0.001*(x[1]-x[-2]), 0.001*(y[1]-y[-2]), color='C0',\n", - " width=0.0001, head_width=0.05, head_length=0.08)\n", - "plt.title(r'Determinate case: $\\phi=1.007$')\n", - "plt.xlabel(r'Real')\n", - "plt.ylabel(r'Imaginary');" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here the winding number is zero: the curve has shifted such that it no longer wraps around the origin at all." - ] } ], "metadata": { diff --git a/notebooks/het_jacobian.ipynb b/notebooks/het_jacobian.ipynb index ae1c365..93f871f 100644 --- a/notebooks/het_jacobian.ipynb +++ b/notebooks/het_jacobian.ipynb @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -30,7 +30,8 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj" + "from sequence_jacobian import create_model\n", + "from sequence_jacobian.models import hank" ] }, { @@ -45,12 +46,21 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "ss = sj.hank.hank_ss()\n", - "household = sj.hank.household" + "blocks = [hank.household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc,\n", + " hank.income_state_vars, hank.asset_state_vars]\n", + "hank_model = create_model(blocks, name=\"One Asset HANK\")\n", + "\n", + "calibration = {\"r\": 0.005, \"rstar\": 0.005, \"eis\": 0.5, \"frisch\": 0.5, \"B_Y\": 5.6, \"B\": 5.6, \"mu\": 1.2,\n", + " \"rho_s\": 0.966, \"sigma_s\": 0.5, \"kappa\": 0.1, \"phi\": 1.5, \"Y\": 1, \"Z\": 1, \"L\": 1,\n", + " \"pi\": 0, \"nS\": 7, \"amax\": 150, \"nA\": 500}\n", + "unknowns_ss = {\"beta\": 0.986, \"vphi\": 0.8, \"w\": 0.8}\n", + "targets_ss = {\"asset_mkt\": 0, \"labor_mkt\": 0, \"nkpc_res\": 0.}\n", + "\n", + "ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { @@ -63,42 +73,28 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "T = 300\n", - "shock_list=['w', 'r', 'Div', 'Tax']\n", - "Js = household.jac(ss, T, shock_list)" - ] - }, - { - "cell_type": "markdown", + "execution_count": 12, "metadata": {}, - "source": [ - "`Js` is a nested dict, with keys on the first level being aggregate outputs:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "scrolled": true - }, "outputs": [ { "data": { "text/plain": [ - "dict_keys(['N_e', 'A', 'C', 'N'])" + "" ] }, - "execution_count": 4, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "Js.keys()" + "T = 300\n", + "exogenous = ['w', 'r', 'Div', 'Tax']\n", + "\n", + "household = hank.household\n", + "\n", + "Js = household.jacobian(ss, exogenous=exogenous, T=T)\n", + "Js" ] }, { @@ -107,23 +103,23 @@ "source": [ "Note that we have Jacobians for four outputs: assets `A`, consumption `C`, labor `N`, and skill-weighted labor `NS` (which is the effective labor provided to firms).\n", "\n", - "We have these four outputs since we did not specify a list of outputs, and the default of the `jac` function is to calculate Jacobians for all outputs reported by the HetBlock (i.e. all outputs of the underlying function `hank.household()`).\n", + "We have these four outputs since we did not specify a list of outputs, and the default of the `jacobian` function is to calculate Jacobians for all outputs reported by the `HetBlock` (i.e. all outputs of the underlying function `hank.household()`).\n", "\n", "For each output—for instance, assets `A`—we have Jacobians for all four inputs we asked about." ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "dict_keys(['r', 'Tax', 'w', 'Div'])" + "dict_keys(['Tax', 'Div', 'w', 'r'])" ] }, - "execution_count": 5, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -141,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "metadata": { "scrolled": true }, @@ -152,7 +148,7 @@ "(300, 300)" ] }, - "execution_count": 6, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -172,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "metadata": { "scrolled": true }, @@ -207,7 +203,7 @@ "metadata": {}, "source": [ "### 1.2 Under the hood: the fake news algorithm\n", - "How did the `.jac()` method calculate all 16 300-by-300 Jacobians so quickly and automatically? Let's take a look inside the fake news algorithm. We'll go over `het.HetBlock.jac()` almost line-by-line, skipping only the saving and loading of saved data (which is not part of the algorithm), and providing additional detail in steps 3 and 4.\n", + "How did the `jacobian` method calculate all 16 300-by-300 Jacobians so quickly and automatically? Let's take a look inside the fake news algorithm. We'll go over the `jacobian` of `HetBlock` almost line-by-line, skipping only the saving and loading of saved data (which is not part of the algorithm) and providing additional detail in steps 3 and 4.\n", "\n", "**Preliminary processing of steady state.** First, there are some preliminaries. (This part is more specific to our code and notation, although useful for understanding the algorithm that comes later.)\n", "\n", @@ -216,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -225,7 +221,7 @@ "{'a', 'c', 'n', 'n_e'}" ] }, - "execution_count": 8, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -244,7 +240,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -260,7 +256,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 18, "metadata": { "scrolled": true }, @@ -268,10 +264,10 @@ { "data": { "text/plain": [ - "dict_keys(['Va_p', 'frisch', 'vphi', 'e_grid', 'a_grid', 'Pi_p', 'w', 'beta', 'r', 'T', 'eis'])" + "dict_keys(['Va_p', 'T', 'e_grid', 'beta', 'a_grid', 'frisch', 'w', 'Pi_p', 'eis', 'r', 'vphi'])" ] }, - "execution_count": 10, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -282,16 +278,16 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "dict_keys(['e_grid', 'pi_e', 'Div', 'Tax'])" + "dict_keys(['Tax', 'e_grid', 'Div', 'pi_e'])" ] }, - "execution_count": 11, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -309,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 20, "metadata": { "scrolled": true }, @@ -320,7 +316,7 @@ "(7, 7)" ] }, - "execution_count": 12, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -340,7 +336,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 21, "metadata": {}, "outputs": [ { @@ -349,7 +345,7 @@ "(dict_keys(['a']), dict_keys(['a']), dict_keys(['a']))" ] }, - "execution_count": 13, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -367,7 +363,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 22, "metadata": { "scrolled": false }, @@ -378,7 +374,7 @@ "((7, 500), dtype('uint32'))" ] }, - "execution_count": 14, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -398,14 +394,14 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 805 ms\n" + "Wall time: 713 ms\n" ] } ], @@ -413,9 +409,9 @@ "%%time\n", "h = 1E-4\n", "curlyYs, curlyDs = {}, {}\n", - "for i in shock_list:\n", + "for i in exogenous:\n", " curlyYs[i], curlyDs[i] = household.backward_iteration_fakenews(i, output_list, ssin_dict,\n", - " ssout_list, ss['D'], Pi.T.copy(), sspol_i, sspol_pi,\n", + " ssout_list, ss.internal[\"household\"]['D'], Pi.T.copy(), sspol_i, sspol_pi,\n", " sspol_space, T, h, ss_for_hetinput)" ] }, @@ -428,7 +424,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 25, "metadata": {}, "outputs": [ { @@ -437,7 +433,7 @@ "300" ] }, - "execution_count": 16, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -448,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 26, "metadata": { "scrolled": true }, @@ -486,7 +482,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 27, "metadata": { "scrolled": true }, @@ -497,7 +493,7 @@ "(7, 500)" ] }, - "execution_count": 18, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -522,7 +518,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -562,14 +558,14 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 50.9 ms\n" + "Wall time: 38.9 ms\n" ] } ], @@ -577,7 +573,7 @@ "%%time\n", "curlyPs = {}\n", "for o in output_list:\n", - " curlyPs[o] = household.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1)" + " curlyPs[o] = household.forward_iteration_fakenews(ss.internal[\"household\"][o], Pi, sspol_i, sspol_pi, T-1)" ] }, { @@ -591,7 +587,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -600,7 +596,7 @@ "(7, 500)" ] }, - "execution_count": 21, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -622,7 +618,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 31, "metadata": { "scrolled": true }, @@ -658,14 +654,14 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 85.8 ms\n" + "Wall time: 93.8 ms\n" ] } ], @@ -673,7 +669,7 @@ "%%time\n", "Fs = {o.capitalize(): {} for o in output_list}\n", "for o in output_list:\n", - " for i in shock_list:\n", + " for i in exogenous:\n", " F = np.empty((T,T))\n", " F[0, ...] = curlyYs[i][o]\n", " F[1:, ...] = curlyPs[o].reshape(T-1, -1) @ curlyDs[i].reshape(T, -1).T\n", @@ -699,7 +695,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 34, "metadata": {}, "outputs": [ { @@ -735,7 +731,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 35, "metadata": { "scrolled": true }, @@ -788,7 +784,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 37, "metadata": {}, "outputs": [ { @@ -804,7 +800,7 @@ "Js_original = Js\n", "Js = {o.capitalize(): {} for o in output_list}\n", "for o in output_list:\n", - " for i in shock_list:\n", + " for i in exogenous:\n", " # implement recursion (30): start with J=F and accumulate terms along diagonal\n", " J = Fs[o.capitalize()][i].copy()\n", " for t in range(1, J.shape[1]):\n", @@ -841,23 +837,23 @@ "## 2 Not using the fake news algorithm? Costlier, direct approach\n", "Suppose that we wanted to get the same Jacobians without using the fake news algorithm. The \"direct\" approach discussed in the paper (which could less charitably be called \"brute force\") uses numerical differentiation to do this.\n", "\n", - "The idea is to run `household.td`, which calculates the nonlinear household impulse response to a given shock, to a small version of each shock (multiplied by $h$) at each date $s$ from 0 up to $T$. We take the results, and rescaling by $h^{-1}$ we get each column $s$ of the Jacobian.\n", + "The idea is to run `household.impulse_nonlinear`, which calculates the nonlinear household impulse response to a given shock, to a small version of each shock (multiplied by $h$) at each date $s$ from 0 up to $T$. We take the results, and rescaling by $h^{-1}$ we get each column $s$ of the Jacobian.\n", "\n", - "One crucial caveat is that since the numerically calculated steady state is not exactly a fixed point of backward or forward iteration, applying `household.td` without any shocks to the steady state does not return exactly the steady state. This numerical error can become quite significant in calculating the Jacobian when we blow it up by $h^{-1}$. We address this below in a simple way: first running `household.td` without any shocks, and then subtracting all the results by this to get the numerical derivative.\n", + "One crucial caveat is that since the numerically calculated steady state is not exactly a fixed point of backward or forward iteration, applying `household.impulse_nonlinear` without any shocks to the steady state does not return exactly the steady state. This numerical error can become quite significant in calculating the Jacobian when we blow it up by $h^{-1}$. We address this below in a simple way: first running `household.impulse_nonlinear` without any shocks, and then subtracting all the results by this to get the numerical derivative.\n", "\n", "Below we differentiate with respect to only a single input, $r$, to avoid making the notebook run for too long." ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 1min 33s\n" + "Wall time: 1min 27s\n" ] } ], @@ -874,12 +870,12 @@ "# (better than subtracting by ss since ss not exact)\n", "# monotonic=True lets us know there is monotonicity of policy rule, makes TD run faster\n", "# .td requires at least one input 'shock', so we put in steady-state w\n", - "td_noshock = household.td(ss, w=np.full(T, ss['w']), monotonic=True)\n", + "td_noshock = household.impulse_nonlinear(ss, {\"w\": np.zeros(T)}, monotonic=True)\n", "\n", "for i in short_shock_list:\n", " # simulate with respect to a shock at each date up to T\n", " for t in range(T):\n", - " td_out = household.td(ss, **{i: ss[i]+h*(np.arange(T) == t)})\n", + " td_out = household.impulse_nonlinear(ss, {i: h*(np.arange(T) == t)})\n", " \n", " # store results as column t of J[o][i] for each outcome o\n", " for o in output_list:\n", @@ -890,26 +886,26 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We see that this took about 90 seconds. Since almost all the work is done separately for each shock, with all 4 input variables in `shock_list` this would take about 360 seconds.\n", + "We see that this took about 90 seconds. Since almost all the work is done separately for each shock, with all 4 input variables in `exogenous` this would take about 360 seconds.\n", "\n", "Compare this to the time needed to get the Jacobian using our fake news algorithm, which is around one second:" ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 939 ms\n" + "Wall time: 859 ms\n" ] } ], "source": [ - "%time _ = household.jac(ss, T, shock_list)" + "%time _ = household.jacobian(ss, exogenous=exogenous, T=T)" ] }, { @@ -926,7 +922,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 43, "metadata": {}, "outputs": [ { diff --git a/notebooks/intro_to_sequence_jacobian.ipynb b/notebooks/intro_to_sequence_jacobian.ipynb index 510a744..c63b670 100644 --- a/notebooks/intro_to_sequence_jacobian.ipynb +++ b/notebooks/intro_to_sequence_jacobian.ipynb @@ -6,6 +6,8 @@ "source": [ "# An introduction to the `sequence-jacobian` toolkit\n", "\n", + "## NOTE: This notebook is outdated with the development of the new `sequence-jacobian` API. Please refer to the other notebooks as of now.\n", + "\n", "This notebook serves as an introduction to the `sequence-jacobian` toolkit and the classes and main functions it provides for solving dynamic general equilibrium models in sequence space.\n", "\n", "This introduction will cover the following topics:\n", diff --git a/notebooks/krusell_smith.ipynb b/notebooks/krusell_smith.ipynb index 8edca00..d831b0d 100644 --- a/notebooks/krusell_smith.ipynb +++ b/notebooks/krusell_smith.ipynb @@ -70,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 58, "metadata": {}, "outputs": [], "source": [ @@ -79,14 +79,15 @@ "import sys\n", "sys.path.append(\"..\")\n", "\n", + "import copy\n", "import numpy as np\n", "from numba import njit\n", "import scipy.optimize as opt\n", "import scipy.linalg as linalg\n", "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj\n", - "from sequence_jacobian import simple, het" + "from sequence_jacobian import simple, het, create_model, estimation\n", + "import sequence_jacobian.utilities as utils" ] }, { @@ -98,16 +99,21 @@ "## 1 Set up heterogeneous-agent block\n", "The main task here is to write a **backward iteration function** that represents the Bellman equation. This has to be a single step of an iterative solution method such as value function iteration that solves for optimal policy on a grid. For the standard income fluctuation problem we're dealing with here, the endogenous gridpoint method of [Carroll (2006)](https://www.sciencedirect.com/science/article/pii/S0165176505003368) is the best practice. \n", "\n", - "Once we have the backward iteration function, we can use the decorator `@het` to turn it into a HetBlock. All we have to do is specify the transition matrix for exogenous states `exogenous`, the policy corresponding to the endogenous state(s) `policy` (currently up to two states), and the backward variable `backward` on which we're iterating (here the first derivative `Va` of the value function with respect to assets)." + "Once we have the backward iteration function, we can use the decorator `@het` to turn it into a HetBlock. All we have to do is specify the transition matrix for exogenous states `exogenous`, the policy corresponding to the endogenous state(s) `policy` (currently up to two states), and the backward variable `backward` on which we're iterating (here the first derivative `Va` of the value function with respect to assets) and a function that initializes a guess for the backward variable, `backward_init`." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ - "@het(exogenous='Pi', policy='a', backward='Va')\n", + "def household_init(a_grid, e_grid, r, w, eis):\n", + " coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis]\n", + " Va = (1 + r) * (0.1 * coh) ** (-1 / eis)\n", + " return Va\n", + "\n", + "@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init)\n", "def household(Va_p, Pi_p, a_grid, e_grid, r, w, beta, eis):\n", " \"\"\"Single backward iteration step using endogenous gridpoint method for households with CRRA utility.\n", "\n", @@ -131,8 +137,8 @@ " uc_nextgrid = (beta * Pi_p) @ Va_p\n", " c_nextgrid = uc_nextgrid ** (-eis)\n", " coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis]\n", - " a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid)\n", - " sj.utilities.optimized_routines.setmin(a, a_grid[0])\n", + " a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid)\n", + " utils.optimized_routines.setmin(a, a_grid[0])\n", " c = coh - a\n", " Va = (1 + r) * c ** (-1 / eis)\n", " return Va, a, c" @@ -140,7 +146,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -162,6 +168,46 @@ "As its name suggests, HetBlock is a general class of heterogeneous-agent blocks that comes with useful methods, such as solving for steady-state policy functions by iteration, updating the distribution of agents across states using these policy rules interpolated against a grid, and computing/storing Jacobians. We are going to cover the the most important methods in this notebook." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The rest of the blocks that constitute the Krusell Smith model are listed below but can also be found in the `krusell_smith.py` module in `sequence_jacobian/models`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "@simple\n", + "def firm(K, L, Z, alpha, delta):\n", + " r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta\n", + " w = (1 - alpha) * Z * (K(-1) / L) ** alpha\n", + " Y = Z * K(-1) ** alpha * L ** (1 - alpha)\n", + " return r, w, Y\n", + "\n", + "\n", + "@simple\n", + "def mkt_clearing(K, A, Y, C, delta):\n", + " asset_mkt = A - K\n", + " goods_mkt = Y - C - delta * K\n", + " return asset_mkt, goods_mkt\n", + "\n", + "\n", + "@simple\n", + "def income_state_vars(rho, sigma, nS):\n", + " e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS)\n", + " return e_grid, Pi\n", + "\n", + "\n", + "@simple\n", + "def asset_state_vars(amax, nA):\n", + " a_grid = utils.discretize.agrid(amax=amax, n=nA)\n", + " return a_grid" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -178,65 +224,19 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, sigma=0.5, nS=7, nA=500, amax=200):\n", - " \"\"\"Solve steady state of full GE model. Calibrate beta to hit target for interest rate.\n", - " \n", - " Parameters\n", - " ----------\n", - " lb : scalar, lower bound of interval bracketing beta\n", - " ub : scalar, upper bound of interval bracketing beta\n", - " r : scalar, real interest rate\n", - " eis : scalar, elasticity of intertemporal substitution\n", - " delta : scalar, depreciation rate\n", - " alpha : scalar, capital share\n", - " rho : scalar, autocorrelation of income process\n", - " sigma : scalar, cross-sectional sd of log income\n", - " nS : int, number of income gridpoints\n", - " nA : int, number of capital gridpoints\n", - " amax : scalar, upper bound of capital grid\n", + "blocks = [household, firm, mkt_clearing, income_state_vars, asset_state_vars]\n", + "ks_model = create_model(blocks, name=\"Krusell-Smith\")\n", "\n", - " Returns\n", - " ----------\n", - " ss : dict, steady state values\n", - " \"\"\"\n", - " # set up grid\n", - " a_grid = sj.utilities.discretize.agrid(amax=amax, n=nA)\n", - " e_grid, pi_s, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS)\n", - " \n", - " # solve analytically what we can\n", - " rk = r + delta\n", - " Z = (rk / alpha) ** alpha # normalize so that Y=1\n", - " K = (alpha * Z / rk) ** (1 / (1 - alpha))\n", - " Y = Z * K ** alpha\n", - " w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha))\n", - " \n", - " # initialize guess for policy function iteration\n", - " coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis]\n", - " Va = (1 + r) * (0.1 * coh) ** (-1 / eis)\n", - "\n", - " # solve for beta\n", - " beta_min = lb / (1 + r)\n", - " beta_max = ub / (1 + r)\n", - " beta, sol = opt.brentq(lambda bet: household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=bet, eis=eis,\n", - " Va=Va)['A'] - K, beta_min, beta_max, full_output=True)\n", - " if not sol.converged:\n", - " raise ValueError('Steady-state solver did not converge.')\n", - "\n", - " # extra evaluation for reporting\n", - " ss = household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=beta, eis=eis, Va=Va)\n", - " \n", - " # check Walras's law\n", - " walras = Y - ss['C'] - delta * K\n", - " assert np.abs(walras) < 1E-8\n", - " \n", - " # add aggregate variables\n", - " ss.update({'w': w, 'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, 'walras': walras})\n", + "calibration = {\"eis\": 1, \"delta\": 0.025, \"alpha\": 0.11, \"rho\": 0.966, \"sigma\": 0.5, \"L\": 1.0,\n", + " \"nS\": 7, \"nA\": 500, \"amax\": 200}\n", + "unknowns_ss = {\"beta\": 0.98, \"Z\": 0.85, \"K\": 3.}\n", + "targets_ss = {\"r\": 0.01, \"Y\": 1., \"asset_mkt\": 0.}\n", "\n", - " return ss" + "ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { @@ -250,14 +250,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 18, "metadata": { "scrolled": false }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXgAAAEGCAYAAABvtY4XAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOydd3RV15m+n3ulq957r0iARJHovXcDNmDcMQaXNNuJM4kn+U3KTDJO1iQzmZnMZGIbgxDY2AbTm+m9SkIC1OioF9Rvr/v3x5EBJchcX5ANeD9reRlJ+5y7xbK/e+7+3u99VUIIJBKJRPLoof6mNyCRSCSSnkEWeIlEInlEkQVeIpFIHlFkgZdIJJJHFFngJRKJ5BHF/ZvewO2EhYWJpKSkb3obEolE8tBQUFDQJIQIv9PPHqgCn5SURH5+/je9DYlEInloUKlUFd39TB7RSCQSySOKLPASiUTyiCILvEQikTyiyAIvkUgkjyiywEskEskjiizwEolE8ogiC7xEIpE8osgCL5FIJN8QwiEwXmhBe6iqR+7/QA06SSQSybcBYXVgKGxEe7QaW6MRt2BP/EbFotLc32duWeAlEonka8Kus6A/WYfuRB0OvRVNtC/BT/fGp38YKvf7f6AiC7xEIpH0MNZGA7qjNejPNIBN4NUnBL+xsXimBKJSqXrsdWWBl0gkkh5ACIH5Sju6I9WYLrSCuxrfwZH4jY5FE+HztexBFniJRCK5jwibA8O5G+iO1GCt06P20xAwJQHfEdG4+Xl8rXuRBV4ikUjuAw6DFd3penTHa3F0WHCP8CF4QRo+WRH3vXnqLLLASyQSyT1gazaiPVqDIb8BYXXgmRaE/4I0PNODe/R83RlkgZdIJJKviBACS0UH2iM1mEqbQa3CZ2A4fmPj8Ij2/aa3dxNZ4CUSicRJhF1gLGlCd6QGS5UWlbc7/hPi8RsZg1uAa+frQggM7W34BgXf593KAi+RSCR3xWGyoc9rQHesBnubGfdQL4IeT8VncCRqDzeX7mm32bh48igF2zdh0utY+l/voVa7dq/ukAVeIpFIusHWZkJ3rBb96XqE2Y5HUgBBc1Lx6huCSu3a+bpJr+P8vl2c+XwruuYmgmPiGDb3SYRD3HfzGFngJRKJ5G+wVGvRHqnBeP4GAN79w/EfE4tHvL/L92xvrOfMji2cP7AHq8lIQr8BTH3lByRnDUal7hmVjSzwEolEgmL8ZSprQXu0Gsu1DlSebviNjsVvdAzuQV4u37f2YhkF2zZx6fQJVGoVfUaNY/DseUQkpdzH3d8ZWeAlEsm3GofFjqGgAd2xWmxNRtyCPAl8LAXfoZGovVwrkQ6HncunT5C/bSN1ly7g6evL0LnzyZoxG/+QsPv8G3SPLPASieRbib3Dgu5ELfpTdTgMNjRxfoQ82wfvfmGo3Fw7X7cYDRQf2MOZnVtob2wgKDKaSUu+Q+aEKXh4ed/n3+DuyAIvkUi+VVjq9OiO1mAoagSHwCsjFP+xsXgkBrg8mNTRdIPCz7dyft8uzAY9sX0yGL/oZVKHDL/vypivgizwEonkkUcIgflSG9oj1ZgvtaHSqPEdFoX/6Fjcw1x/sm64epn8bRu5ePIoQgjSh49m8OwniO7V26nrLSYbZcfqaKnTM/GFPi7voztkgZdIJI8swurAUNSI9mgNtgYDan8PAqYn4Tc8CrWPxrV7OhxcOZNHwfaNVJcW4+HtTfaMOQyaOZeA8Ain7qFtMXHuQDWlR2qwmOzEpAVhs9px10gdvEQikXwpdr21M1ijFofOiibKl+CF6fgMDHc5WMNqNlFyaD9ndmyita4W/7Bwxi96mf6TpuPp45z9741KLYV7KrlS0IgAUgeFkzUlgcikAJf2dDdkgZdIJI8M1hudwRoFjWBz4NU7WAnWSA1y+Xxd19pC0a7tnN2zA5NOS1RqGo/98G3Sh49G7Xb3J27hEFQUN1O0t5Kai21ovNzoPzGOAZPiCAjt2carLPASieShRgiB+Wo7uiM1mMpbwF2Fb3YkfmNi0ES6bvx1o+IaBds3U37sIHa7nV5DRjB49hPE9s5w6s3CZrFTfrKes/uqaGsw4BfsyagFvcgYE4On99dTemWBl0gkDyXC7sB4rgnt0RqsNTrUvu74T07Ab6TrwRpCCCrOniF/+yYqzhXi7ulJ/8kzGDzrcYKiop26h6HDwvlD1RQfqsGksxKe4M/UlzNIHRSBm9udj4eExYLK4/6HgfRogVepVNcBLWAHbEKIIT35ehKJ5NHHYbShP12H7lgt9g4L7uHeBM3vhW92BCoXm5Q2q5Xyowcp2L6JpqoKfINDGPPMiwyYOhNvP+fsCVpq9RTtq+TiqQbsdgdJ/cPImhJPTFr3x0PGoiKaV+Zirawkaf1n990//ut4gp8ohGj6Gl5HIpE8wthaTMr5en49wuLAMzWQoPlpeKUHu2z8ZdRpObdnJ4Wfb0Xf1kpYQhIzvv8WfUaPw8397iobIQTVF1op2lNFZUkzbho1fUZFkzU5nqDIOzdehd2Odt8+WnJWYiwsRO3vT/DTTyGs1vv+FC+PaCQSyQONuaID3ZFqjCXNoPoiWCMWjxg/l+/ZVl9HwY5NFB/ci81sJmngIGb84Mck9s9y6inabnNwOb+Bon1VNFXp8PbXMGxOMv3Gx+LdzfGQQ6+nbcNGWlatwlpVhSYujsj/9/8IWjAftW/PhIT0dIEXwG6VSiWA94QQ7//tApVK9RrwGkBCQkIPb0cikTwMCMdtwRqVWlRe7viPj1OCNQI9Xb5vzYUy8rdu4HL+Sdzc3OgzZgJDHnuCsIQkp6436a2UHKnh/IFq9O0WgqN9mbioD+nDIrvVsFsbGmj98ENaP12Lo6MD76wsIn7yE/ynTEblhArnXlAJIXru5ipVjBCiVqVSRQB7gDeEEIe7Wz9kyBCRn5/fY/uRSCQPNg6zHUN+PdpjtdhbTLiFeOE/JlYJ1vB0rRj+rfGXl68fA6fNImv6bPyCQ5y6R/sNI2f3V1F2vA6b2U5cn2CypiaQkBHS7RO/qayM5pwcOnbsBIcD/6lTCXlpMT7Z2V3W3TDcoLylnLFxY136/VQqVUF3/c0efYIXQtR2/rtRpVJtBIYB3RZ4iUTy7cTeYUF3vBbdqTqE0YZHYgBBs5Lxygh1+XzdYjIqxl87Nt8y/lr6XfqNn4LGyzn737or7RTtreRa0Q1UahXpQyMZOCWesLg7N16Fw4Hu8GFaclZiOHUKlY8Pwc89S8iiRXjEx3dZe6HlAqtKV7Hj2g683b058NQBPN1c/3RyJ3qswKtUKl9ALYTQdv55GvCbnno9iUTy8GGt16M9csv4yzszFL+xcXgmuj7ZqWtppvDzrZzduxOzXk9Met+vZPzlcAiuFt6gaG8lDdc68PRxJ3t6IgMmxOEbdOcC7DCZaN+yhZaVuViuXsU9MpKIn/6EoIULcQu49bsIIThee5zcklxO1J3A292bhekLWdR30X0v7tCzT/CRwMbOjy/uwBohxOc9+HoSieQhQAiB+Uob2sM1mC+23jfjrxsV18jftpHyY4cRDgdpw0YyePY8YtKdM/GymGyUHa/j3P4qOppMBIR5MfbpdPqMjMKjG194W3MzrWs+pvXjj7G3tOCZ0ZeYP/6BgBkzUGluqXAsdgvbr25nVekqLrddJtw7nB8O+iEL0xcS6Bno8u98N3qswAshrgIDe+r+Eonk4ULYHRjONaE7XI21To/aT0PAtER8h0fj5uui8ZcQXD97hvxtG6k8X4TG04uB02YyaObjBEVGOXUPXati/FVypBaL0UZ0aiCjF6SRNDAMdTfHQ+bLl2nJzaV98xaExYLfxImEvPQSPsOGdjmTbzO18emFT/m4/GOaTc2kB6fzzph3mJk0E42bBmxmKPwIbpTDtN+69HfwZUiZpEQi6VEcJhv60/XojtVgb7fgHuFN8II0fLIiUGlcM/6642DSs4sZOGUmXn7OySdvVGkp2lvJ5bxGhBCkZEeQNTWeqOQ7P1ELITCcPElzTg76w0dQeXoSOG8eIYsX45mS3GXt9fbrrC5dzZYrWzDZTYyOHc3ijMWMiB6hvAEYWiB/OZxeBroGiOwPVhNoXI8GvBOywEskkh7B1mZCd7QWfV49wmzHMyWQoHn3OJik7eDsnp0U7drm2mCSQ1BR0kzR3ipqLrSi8XSj34RYBk6KJ6Cb4yFhsdC+YwctK3Mxl5fjFhpK2JtvEPzMM7iH3FLhCCEoaCggtzSXQ1WHcFe7Myd1Dov6LqJXcC9lUfMVOPl/ylO7zQipk2Heu5AyEe7zFCvIAi+RSO4zlhod2sPVGM/fAMB7QDj+Y+PwiHV9MKm1vpYzOza7PJhks9q50Gn81VpvwDfIk5HzU8kcE4NnN77w9vZ2WteupXX1h9gaG/FM60X0O/9KwOzZqD1vNUStDit7ru9hVekqSppLCPIM4rUBr/FMn2cI8w4DIaDiOBz/X7iwA9w0MOApGPEDLKF92HauloufX+BnM2Xgh0QieQARDoHpYiu6w9WYr7aj8nTDb3QsfqNjcA9y7dhBCEHthTLyt210eTDJ0GGh+HANxYeqMWqthMX7MWVJBr2GdG/8ZamupiV3FW3r1yMMBnxHjST6nX/Fd8yYLm8mWouWDZc28GHZh9Tr60kKSOKXI37JnNQ5eLt7g90GxeuVwl57BrxDYNxPYOirtLuHsOZUJSuP76ehw0yfKH9+NCUNLxn4IZFIHhRuJiYdqcbWaMQt0IPAWcn4DotC3Y3y5G447HYunT5BwbaN1F1WBpOGP7HwKw0mtdbrKdpbxYWT9dhtDhL7h5I1JYHY9C8x/jp7luaclWh37wa1msDHZhHy0kt49e3bZV2trpYPyz5kw6UN6K16hkQO4Z+G/xPj4sahVqnB1AGnl8Opd6G9CkJS4bE/wcBnqdLB8gPXWJtfiMFiZ3SvUP5twQDGp4ffd6MxkAVeIpG4wN8lJkX7EvJ0b7wHhKHq5sn4btzrYJIQgpqLbRTtraTivGL81XtkFFmT4wmOurPXi7Db0R04QHPOSowFBaj9/QlduoTgF15AE9VVhVPcVExuSS57KvYAMC1pGoszF5MZmqksaKtSivqZVWDugMTRMPMPkD6Dwup2lq0r4/PietQqFXMHxvDy2GQyY3pOIgmywEskkq+ArcmI9mgNhoIGhPWLxKQ4PFMDXX4C1bY0Ufj5Ns65OJhktzu4nN9I0d7Km8ZfQ2cn0398LN7+3Rh/GY20b9qkDCZVVKCJiSHy5z8jcMGTuPndejOwO+wcrD7IqpJVnGk8g5/Gj0UZi3iuz3NE+3X6w9ecgRP/CyWblK8z58HIH2CPzmZPaQMfvHeS/IpWArzceW1cKi+NSiIq8P6qZbpDFniJRHJXzBUd6A5XYyxtBrUKn+wI/MfG3lNiUuP1qxRs20j58SMuDSaZDVZKjtRy7kA1+jYzwVE+THi+N72HR+Hucec3BltTE61r1tC65mPsbW149e9P7H/+Cf+pU1G53yqHBquBLVe2sLp0NZXaSmJ8Y/jpkJ8yP20+fh5+4HBA+Q6lsFccA88AGPE9GP5dDD7RfFZQzYo1B7nebCA+xJtfz8ngqSHx+Hp+vSVXFniJRHJHhENgKm1Ge7hacXT0dsd/Qrzi6BjgemLSvQ4mdTR1Gn8dq8NqthPbO5gJz/cmMbN73xrzlSu0rFypDCZZrfhNmkTokpfwHjy4yyePG4YbfFz+MWsvrqXd3E7/sP78cdAfmZIwBXe1O1gMkPcBnPg/aLkCgfEw/XeQvYhGiwe5J67z0an9tBmsZCcE8faMPkzLiMTdxWOre0UWeIlE0gWHxY6hoAHt0RrszYqjY9DcVHyGRKLu5sn4btisVsqOHqBg2yaaqytdGkyqv9pO0d4qrhY2olKp6DU0gqzJCYQndGP8JQSGU6dpzlmB/tBhZTBpfudgUnLXwaSLrRdZVaIYf9kcNibGT2Rx5mKyI7KVNwBtA+Qtg7zlYGyBmEHw5Aro+zgXbhhZtvUqW4pqsTocTMuI5LVxKQxOdK4h3JPIAi+RSACway3oTtSiP1mHw2DDI8GfwBnJeH/Jk/HduNfBJIdDcP1sE0V7K6m70q4Yf01LoP+EePyC72zOJaxWOj7/nOacHMylZbiFhBD2xusEP/vs3w0m3W785eXmxfy0+SzKWERiQKKyqKEUTvwFzq8FuxV6z4JRryPiR3D0SjPLcs9w+OINvDVuPDMsnqWjk0kK65nwDleQBV4i+ZZjbeh0dCxUHB29MkLxH3dvjo6t9bUUbN9MycG92CxffTDJarFz4UQdRXuraL9hxD/UizFPpdF3VHS3xl92rZa2tetoWb0aW309HikpRP32NwTOndtlMMlit7Dj2g5yS3K53HaZMO8w3sx+k4XpCwnyClIGk67sV/TrV/aBuzcMehFGfB9LYDJbztbywcajlNdrCff35KfTe/P88ASCfO5/aPa9Igu8RPItRAiB+Wo7usPVmC50OjoOjcJvTCwaFx0d79tg0qFqzh+qwaSzEpHoz/RX+5GSFYa6m3Nsa20tLatW07ZuHQ69Hp/hw4n651/jN24cKvWta9rN7ay7uI6Pyj6iydhEr6Be/Hb0b5mVPAsPNw+wWaDoY6Vx2lAMfpEw6Rcw5GXa8OOjU5XkHt9Po9ZM70h//vjkAOZmxeDp3rOpTPeCLPASybcIYXdgPN+E9kgN1hodal8NAVMT8R3huqOjw2Hnct5J8rduuJmY5Mpg0tl9VZSfrMdudZA0IIzsqQlE9+pefmk8X0xLTg4du3YBEDBzJiFLXsI7M7PLumpt9c3BJKPNyMjokbwz+h1GxoxU7m1sg4K/wKn3QFsH4X3h8b9A/4VUtNtYsecaa/OrMVrtjE0L448LBzIuLaxHBpPuN7LASyTfAhwmG/q8enRHa7G3m3EP9yZ4fho+2a47OlrNJkoO7qNg+ybaGuoIjIxi0pLv0G/CVKcHk+qutFO0p5Jr55pwc3NiMMnhQHfwEC05ORjy8lD7+hLy4ouELHoBTUxMl7Xnb5xnZclK9lbuRY2amckzWZy5mN4hvZUFrRW3BpMsOkgeD3P/F3pNpqCylWUfF7O7tB43tYq5A2N5ZWwyfaNdP7b6JpAFXiJ5hLG1mdEdr0F/6jZHxydS8eod4nLj1NDeRuGu7RTt3o5J20FUr3TmPLeYXsNGupSY5OWrYcisJPqPj8OnG/mlw2SiffMWWlauxHLtGu7R0US8/TZBC5/Ezf+WisYhHByqOsTKkpWcaTyDv8afxZmLea7Pc0T5dsowawqU8/XSTaBSQ78FMPJ17JH92V1Sz7K/HudMZRuB3hq+Oz6VxaOSiAz4egaT7jeywEskjyCWWh26IzUYzt4ABN79w/EfG4tHN1miztBSW0PB9o2UHtqPzWohdchwhsyeR2yfTOcap2Y7ZcfrOLuvko4mE4Hh3ox/Np3eI6PRdDeY1NKiJCatWYO9pQWvjAxi/vhHAmZM75KYZLKZbg4mXe+4TrRvNG8PfZv5afPx1fgqg0kXdsLx/7k1mDTydRj+XfRekazLr2LF6oNUthhICPHhX+Zm8uTguK99MOl+83DvXiKR3EQIgflyG9rD1ZgvtaHycMNvZDR+Y2JxD3b9CbSmvJT8bRu4nH8KN3d3MsZNYvBjTxAaG3/3iwF9u5nzB6spPlSD2WAjKiXg7olJV68piUmbNiHMZvzGjydk6dK/S0xqMbXwSfknfFL+Ca3mVjJCM/jDuD8wNXGqMphkNUJ+jiJ1bL7UZTCpweLByuPX+ehkMR0mG4MSgvj5zD5My4zCzcVPNw8assBLJA85wu7AeK4J7RdReP4aAmYk4Tc8GrW3i46ODjtX8k6Rt20DdRfL8fLzZ8S8p8iaPhvfoGCn7tFSq6doXyUXTtXjsAtSssLJmpJAdGr3iUnG/Hyac1ai278flYcHgY8/TshLi/FMTe2y9lr7tZuJSWa7mfFx41mcuZghkUOUNwB9kzJxenoZGJogeiAsWA4Zj1PWaGTZlqtsPVuL3SGYnhnFK2NTGJzo3O/1MCELvETykOIw29CfblCi8NrMt6LwsiNQud9D4/TQfgq2b6StvrNx+hUdHWsvtlHY6ejorlGTMTqGgZPiCYr0ufM1Nhva3btpXpGDqbgYt6Agwr7/fYKfexb3sLAu9z7TeIaVJSs5VHUIjVrDnNQ5vJjxIilBKcqipstw8i9QtAZsJkibDqPeQCSO5vDlZj5YeYYjl5rw8XDj+eGJLB2dTELonff1KCALvETykGHvsKA7XoPuZD3CZMMjOYCgx++xcdrRTtGubRTt2o7xi8bpW1+hcWp3cOXMDQr3VHKjUou3v4Zhc5LpNz4Wb787N07tOj1tn62jddVqrLW1eCQmEvXPvybw8cdRe9/S4tscNvZV7iO3JJfzTecJ9Ay8Q2LSCeV8/YvEpIHPwMjXMQf3YnNRLcs3HeVCg5YIf0/entGb54clEthNktOjhCzwEslDgrXRgPZw9c2JU+9+YfiPi8Mj/v41TlMGD2PonPlON04tJhtlx+o4u68KbYuJoMi7Ozpa6+tpWb2atrXrcGi1eA8ZTOQ//T/8Jk7sMphksBrYeHkjq0tXU6OrIcE/gV8M/wVze829lZhUslEp7DUF4B0M434Kw16lVRXER6cqyD1xgBtaJTHpPxYOZM7AGDxc/HTzMCILvETyACOEwHKtA+3hakzlLTcnTv3HxuIe6trEKUDNhTLyt65XGqdubkrjdPY8pxunulYz5w9WUXy4FovRRkxaEGOfSSepX/e+NaayMppzcujYsRMcDvynTyN0yRK8Bwzosu6G4QZrytfw6YVP0Vq0ZEdk89MhP2VC/ATc1G5g1kH+u8pRTFslhKTAY/8BA5/jeodg+d5rrCsowGR1MC49nD89lcyYXg/HYNL9RhZ4ieQBRDgExpImtIdrsFZpUfu6EzAlAd+RMfc0cXol/xR5Wzsbp75+DH/iKbJnON84ba7RUbSnkot5DQiHICU7gqyp8UQld9841R89SvOKFRhOnETl40Pwc88S8uKLeMTFdVl7qfUSuSW5bL+2HbvDzpTEKbyY8SJZEVnKAm29Mm2avxxM7RA/HKb/DpE+k4KqDt7/pJQ9ZQ1o1Goez4rhlbEp9I5y/dPNo4As8BLJA8TfWfWGehH0RCo+g1y36rVazJQeUiZOW+tqCYyI/MoTp9XlrRTtqaSytAV3DzWZ42IZOCmewPA7f4oQFgvt23fQsmIF5kuXcI+IIOIn/0DQU0/hFhDQ5d4n606SW5rLsZpjeLt7szB9IYv6LiI+oPPTREOp4g9zbi0IO/SZDaPewBYzhF0lDSx79xRFVW0E+Wj4wYRevDgqkQj/h3Mw6X4jC7xE8gBg11nQnahDf7IWh96GR/y9W/X+XeM0NY3ZP/oZacNGonZzPgqvcE8lzdU6vAM8GP54Cv3GxeLVzacIxdFxLS2rVmNraMAzLY3o3/+ewMdmofK41Wy1Oqx8fu1zcktyudB6gVCvUN7IfoOn0p+6zdHxgHK+fmUfaHxgyBIY8T10vgmszatixZqDVLcaSQr14bePZ7JgcBw+HrKk3Y7825BIvkG+yDjV5zeAzYFX3xClcZoU4PKZcWtdDQXbN1FycN+txuns+cT2da5xajbaKD1Sy7kDVehalSi8iYv60HtYFG7d+NZY6+oUR8e1axVHxxEjiP7X3+I7ZkyX19RatKy/uJ4Pyz6kwdBASmAKvxn1G2alzMLTzVNxdDz7iVLYG4rBN+Kmo2ODzadzMGkfHSYbQxKD+eXsDKb0jXxkBpPuNz1e4FUqlRuQD9QIIWb39OtJJA8D5srOjNOS2zJOx8WhiXBdk600TjfctOpVJk7nERrnXONU22Li3P4qSo7WYjXZie0dxPjnvjwKz1ReTvOKFUrjVAgCZswgZOmSv3N0rNPV8WHZh6y/tB69Vc/wqOH8auSvGBM7BrVK3eno+FfF/EtbB+F9FOOvAU9xsdnCsu1X2VRUg90hmNEvilfHppCd8OgNJt1vvo4n+B8CZcDDZcMmkdxnhENgKm9RMk6vd6Dycsd/fDx+o1zPOP2icZq/dSO1F8tcapzeqNRStLeSy/mNCKDX4AiypsQT0U3ghxAC/fHjtCxfgf74cVQ+PoQ8/xzBi17EIy62y9rS5lJWlqxk9/XdAExPms7izMVkhGYoC9oq4eS7cCa309FxHMz9H0TqZE5ca+H91Wc5eOEGXho1zw1LYOmYZBJDH5zEpAedHi3wKpUqDngMeAf4cU++lkTyoCJsDgyFjWgPV2O7YcQtyJPA2Sn4Do1E7aKZ1d82TgPCI5n40nfoP9H5xmllaQtFeyqpLm9F4+lG/4lxDJgUR0A38kthtdKxcyfNK3Iwl5fjFh5G+FtvEfzM07gF3lLROISDozVHyS3J5XT9aXw1vrzQ9wWe7/s80X7RyqKaM0rjtGST8nW/BTDqdWwR/dlRXM/7fzlGcU0HYX4e/MPUdF4YkUiw74OXmPSg09NP8P8FvA18u7VKkm8lDoMV3al6dMdrcGitaKJ9CXmmN979w1B1k050N5TG6XaKdm3DqO0gMiWN2T/6R9KGjXKucWpzcCmvgcI9lbTU6vEN9GDkvFQyx8bg2c1kp12no23dZ7SsWoWtrg6P1FSi3/lXAubMQX1b49RsN7P96nZyS3K52n6VSJ9IfjLkJ8xPm4+/h3+no+PnnY6OR8HDH0Z+H4Z/F51XFJ/mVbEi9yA1bUZSwn35/fz+zMuOxUvz4CYmPej0WIFXqVSzgUYhRIFKpZrwJeteA14DSEhI6KntSCRfG7ZWE7qjNejz6hEWB55pQfg/FYdnryDXG6f1tRRs20TJoX3YLGZSBg1l6JwFX6lxWnKkhnP7qtC3WwiN9WXyS31JGxKJWzeTndaGBlpXr6b107U4tFp8hg4l6te/+rsovDZTG2svrmVN2RqaTc30CenD78f+nulJ09GoNWA1QcFKxdGx6SIExMG0d2DQizRaPMi5rXE6LCmEf56byeQ+Ed06TUqcRyWE6Jkbq1S/BxYBNsAL5Qx+gxDihe6uGTJkiMjPz++R/UgkPY2lRof2SOnizmMAACAASURBVDXGczcAFT4Dw/EbG4tHjJ/L96y/fJG8Leu5ePo4bm5u9B07iSGznW+c6lrNnNtfRfGRGqwmO3F9gsmemkB8Rki3bwymixdpWZFD+/btYLcrE6dLl+Ldv3+XddXaalaVrmLT5U0YbUbGxI5hceZihkcN73R0bFYcHfOWgf4GRA2AUW9C5hNcbDKx7LBsnN4PVCpVgRBiyJ1+1mNP8EKInwM/79zABOAnX1bcJZKHESEE5kudHuyXOz3YR8UqHuxBni7f83pRAXlb1lNVeh5PH1+GPf4k2TPmOJ1x+rcTp70GR5A9LZHwhDuflgohMJw6RfPyFeiPHEHl7U3w008TsvhFPOK7vpmUNJWQU5LDnoo9qFVqHkt+jMWZi0kLTut88SvK03rRGrAZIW1ap6PjGE5ca2HZqkIOdDZOnx2WwMuycdpjSB28ROICwi4wnr+B9tAXHuwe9+zBbrdZKT92mPytG2iqqsAvNIzxi15mwOTpeHjfXT4phKD2UhuFezqtejsnTrMmxxMQ1k3j1Gaj4/NdtKxYgam0FLfQUMJ/+CZBzzyDe3Bwl3sfqTnCypKV5NXn4afx46XMl3i+7/NE+EQoi6rz4dh/Q9lWxdFxwFMw8g1soensKK5n2V+Oc76mnVBfD348NZ1FsnHa4/TYEY0ryCMayYOOw2LHkN+A9kg19lYlvNp/XNw9ebBbjAbO7dtFwY7N6JqbCItPZOjcBfQeNRY397v7znyRcVq4p5LG6x14+2voPyGO/uPj8PK78/UOvZ629etpWZmrWPUmJxOy5CXFqtfz1icPq93K9mtK4/Ry22UifSJZlLGIBWkL8PPwUxqnl3bBsT9D5XHwDIShS5UoPI8wPs2rYvnRa0rjNMyXV8amMH+QbJzeT76RIxqJ5FHCrreiP1GL7ngtDoMNjwR/gman4tXXdQ92fVsrZ3Zu4ezuHZgNeuIz+jPt1ddJyhrsVOPUZrFTfqKOwr1VdNwwKhmnz/Wmz4gvseptbKT1w49o/eQTHB0deA8eTOQv/gm/CRO6NE61Fi3rLq7jo9KPaDQ2khacxu/G/I4ZyTOUxqnNDGdWK4qYpgtK43T675TGqVnDymPX+fDkWTpMNoYmBcvG6TeELPASyZdga7lNEWPttBIYH4dn0p3dE52hpbaa/G0bKT20D7vdTvqwUQyZO5/oXr2dut6ks3L+UDXnD1Zj1FqJSApg1LxUkrPCu884vXJFmTjdshVhs+E/dSqhS5fgnZXVZV29vp6Pyj5i3cV1ysRp9HB+M/o3jIoZpbzpGNsgf4Xi6qirh8j+MH8ZZM7jUpOJ97dcZXNRLVaHgxmZUbw6LoVBsnH6jSELvERyByy1OrSHb1PEZIXjPz4OTaTrzcDai+XkbVmvWAm4u9Nv4lQGP/YEwdGxd78Y6GgyUrS3irLjtdgsDpL6h5I9LYHobuSXQggMeXm0rMhBd/AgKk9PAp9cQOhLL+GRmNhl7cXWi+SW5LLj6g4EgmmJ03ip30u3TZxWwcm/3po4TZkI8/6KSJ7AyWutvH9b4/SZYfGycfqA4FSBV6lU84F/AyIAVec/Qggh7QckjwxCCMxX29EeqsZ8sfX+KGIcDq4W5pG3ZQM15SV4+fp95fDqxooOCvdUcqWgEZVaRfqwSLKmJhDajfxS2Gxo9+6lefkKTOfP4xYcTNjrrysZpyG3VDhCCPLq81hRsuKmVe/TfZ5mUcYiYv0633Tqzyvn68Xrla/7LVCseiP63bFx+sKIREJk4/SBwdkn+D8Ac4QQZT25GYnkm+BmuMahaqzVOtR+GgKmJyqKGBdzO+02K2VHD5G/dQPN1ZX4h4UzcfGr9Js0DQ+vuycxCSGoKm3hzO5Kai604uHlRtbUBAZMjMcv+M5vNg6DgbYNG2lZuRJrdTWaxIRuM073VuwlpySH0uZSQrxCeCP7DZ7u/TSBnoGdVr37lcJ+9QB4+MHw78KI76H3jlYap19MnIb58rt5/WXj9AHF2QLfIIu75FFDWB3ozzSgO1KDrcmohGvM64XvoAhULhYrs0HPub2fc2bHZnStLYQnJDHr9X8gfeRY3Nzv/r/bTQ/23ZU01+jwDfRg1PxeZIyNwbMb+aWtuZnWjz6i9aM12Nvb8c7KIuIf38Z/0iRUt9kX/G3GaVJAEr8e+WvmpM5RrHrtViVU4/iflSd3v0iY/GsYsoRGqzcrj1/nw86J06FJwfx6jmLVKxunDy7OFvh8lUr1KbAJMH/xTSHEhh7ZlUTSgziMNnQn69Adq8Ghs6KJ8yPk+T54Z4a5rIjRtTQripg9O7EYDST0G8j07/2IxAHZTodXlx6t5ew+xYM9JMaXyYv7kja0eysBS2UlzTk5tG/YiLBY8Js8idClS/EZNKjLumZj882M03ZzO1nhWfx06E+ZGD9Rseo1a+H0B8oZe3sVhPW+adV76Qur3kLZOH0YcbbABwAGYNpt3xOALPCShwZbu1lRxJyqR1jseKYH4z8uDs/UQJc9Ypqrq8jftoHSwwcQDgfpI0YzdO4CIlN6OXW9vt3MuQPVlByuwWxQwqvHP9ebxH6h3e7JeL6Y5uXL0e7ejcrNjcAnHidkyVI8U5K7rLvefp3c0ly2XN6C1WFlYvxElvRb8jcZp+8qqhhTOySMgln/jkibyslrbSz78Bz7yxvx0qh5eqjSOE0Kk43ThwmnCrwQYklPb0Qi6SmsDXq0h2swFDWCEHgPCFdSk+7BI6amvJS8reu5kn8Kdw9PBkyZzuDH5hEUGeXU9a31eor2VFJ+qh5hF6Rkh5M9NZHI5C/xYD96jOYPPsBw6hRqf39CX36Z4EUvoImI6LK2qLGIlSUr2V+5H41aw9xec3kx40WSAzvfAG5cUI5hzq1VjmX6zoHRP8QWPYidxfUs+78TnKtWGqdvTUln0UjZOH1YcVZFEwf8DzAa5cn9KPBDIUR1D+5NIrknzNcVRYyprAWVRo3vsCj8x8bhHuJaILNwOLhScJq8LeuVcA3/AEY++SxZ02fjE+CcLr7usmIlcO1sE24aNRmjYhg4JZ6gbpKchNVKx+ef0/zBcswXLuAeGUnE228T9NRC3PxuvUE5hIODVQdZWbKSwsZCAjwCeKX/KzzX9znCvMOUxun1Y0phv/g5uHtB9iIY+QMM/olK4/QjJeNUNk4fHZw9oskB1gALO79+ofN7U3tiUxKJq9xMTTpUjaWiA7WPO/6TE5TUpG6Cou+GzWql9PB+8rdtpLW2moDwSCYt+Q79JjgZruEQXDvXROHuSuqvtuPlq2HIY0kMmBCHt/+dn4wdBgNtn31G88qV2Grr8OiVSvTvfkfg7Me6hFeb7Wa2XtlKbkku1zuuE+Mbw8+G/Yx5vebho/EBh10J1Tj+Z6gpAO8QGP8zGPYqTcKfVcevs+rkftoMVoYkBvOr2bJx+ijhlBeNSqUqEkJk3e1794r0opG4irA5MBR1piY1KqlJ/mNj8Rkahbqbsf27YdLrOLtnJ4U7t6BvayUiKZWhc+eTPmKMU+EaNqudi6eUcI22BgMBYV4MnJxA31HRaDzvfP3fKWKGDCb05ZfxGz++i5VAu7mdTy98etODvW9IX5b0W8LUxKm4q93BYoCijxRXx9ZrEJwEI1+HrOe53iH44OhV1uVXY7E7mNo3ku+MT2FwonNOlZIHi/vhRdOkUqleAD7u/PpZoPl+bE4iuRccZhv6U/XojtZg77Cgibr31CRdSzMFOzZzds9OrCYjiQOymfmDfyCh/0DnwzUO13B2XxWGDgvhCf5MeyWT1Oxw1N3s6Y6KmJdfxic7u8u6Gl0Nq0tXs+HSBow2I6NjRrOk3xKGRQ27zYN9GZx+HwzNEDsYpvwz9J3D2Rot768rZ2dxHe5qNfMHxfLK2BR6Rbjei5A82Dhb4JcC/wv8J8oZ/PHO70kk3wh2nQXdsVp0J+oQJhueKYEEL0jDMz3YZUVMS201eVs2UHZkPw67g/SRYxRFTHKqU9fr282c3VdFyeEaLCY78RkhTJmWQFzv7vdkPF9M84rlaHfdrohZgmdKSpd1Zc1l5JTksPv6blSomJk8k8WZi+kd0ulf03JVeVov/EjxYE+fAaPeRCSM5NClJt77II8TV5vx93LnO+NTWTIqiYgA13oRkocHZ1U0lcDcHt6LRHJXbK0mtIerMeQ3IGwOvDNC8Z8Qj0e867G/9ZcvcnrLZ1w6fQJ3dw39Jk1nyGznFTFtDQYK91RSfrIOYRekDo5g0F3CNfRHj9G8fDmGkydR+/kR+vJSghct6qKIEUJwqv4UK86v4ETdiZvh1S9kvECUb+feqgvgeKcHu9r9pge7NTSdrWdreX/TUcrrtUQFePFPs/ryzLB4/L1c60VIHj6+tMCrVKq3hRB/UKlU/4Py5N4FIcSbPbYzieQ2rA16tAerMZxtBFT4ZEco5l/dqE/uhhCCyvNnOb15HZXFZ/H08WX4EwvJnjHHaY+YhusdFO6q4ErRDdzcFUVM1tR4AsO/TBGzi+blyzGXl+MeEUHET39K0NNPdVHE2B129lbuZUXxCkqbSwn1CuWHg37IU72fIsAjQFHEXNythGtUHFU82Ef/UAmv9gjjk9OVrDh6gNp2E+mRfvzHwoHMGRiDh4t+9ZKHl7s9wX9hTyA7n5JvBHNlB9qD1ZhKm1Fp1PiNjMFvbJzL5l8Oh51Lp05wevM6Gq9dwTc4hHHPL2HAlJl4+jiXmlRV1sKZXYpHjKePO4OnJzJgUjw+AV+miFmveMTU1uKR2r0iZvPlzawsWUmVtooE/wR+NfJXzE2de8tK4OynSmFvLIGAWCW8evBiblg8WHn8GqtPKB7sw5NDeGdefyb0Dnf5yEry8POlBV4IsbXzjwYhxLrbf6ZSqRbe4RKJ5J65mXN6sArz1XZU3vdL6riPvC3raauvIzg6hqmvvUHGuEm4a5xITbI7uFJ4gzO7Kmiq6vSIWdCLzLExeHh14xHT0kLrhx/eUsQMHkzkL36B34SuipgOSwefln/Kh2Uf0mJqoV9oP96a8BaT4ifhpnYDix7yVihn7O1VEN4Hnvgr9HuSq60Wlm2/xvoz1VjtipXAa+NkeLVEwdkm68+BdU58TyJxGeEQGIs7XR1rdKgDPAh8LBnfYdGou5EV3g2zwcDZPTs4s3ML+tYWIlN6Meetn9Fr2EjUaiekjl+kJu2ppKPJRFCkDxMX9aH3sCjcNN0rYlpWrqRt/QaE2YzflMmELn0Zn0FdFTEN+gZWl65m3cV1GGwGRseMZmm/pQyNGnpLEXP6PUURY2yFhJEw698hbRqF1e289/F5dpXWo3FT8+TgOF4dm0KytBKQ3MbdzuBnArOAWJVK9efbfhQA2HpyY5JvD8LmwHCmU8PeZMQ9zJvgBWn3lHP6t3F4Cf2zmPmDH5PQzzmpo0lvpfhwDef2V2HUWolMDmD0k2kkD+jekMxYXELz8g/uqoi52naVnJIctl3dhhCC6UnTWdJvCX1C+igLWq8rT+tnViuKmN6zYPSPcMQN4+DFRt5ddorT11oI8HLnBxN6sXhUEuH+rh1ZSR5t7vYEX4ty/j4XKLjt+1rgrZ7alOTbgcNsR3+6Du2RGhwdFjSx9+7q2NZQT/7WDRQf3IPdZiN92CiGPv4kUalpTl2vazVzdl8lJUdqsZrtJGSGMmh6AjFp3acm6Y8dp3n5BxhO3KaIeWERmsi/94hZXrycg1UH8XLzYmH6Ql7MeJE4/zhlQf15OPpfULIRVGoY8DSMfhNLcBpbztby/vrDXGzQERPoxS9nZ/D00Hj8PGUom6R77nYGfxY4q1Kp1qCkOPVBUdNcEEJYvob9SR5B7HoruuNKgLUwKhp2/yfT8eymiDpD4/WrnN78GRdPHEXtpiZj3CSGzFlASIxzcXit9XoKd1dy4VQ9QkCvwREMmp5AWFw3Uke7He3u3TQtW4a5tKxbRYxDODhSfYQVxSs403iGQM9Avjvwuzzb51lCvEIURcy1w0phv7JPCdcY8T0Y8X20nhF8crqK5UcPUN9hok+UP//59EBmD4hB4+IQl+TbhbNv/1OB94ArKIU+WaVSfUcIsbPHdiZ55LC1mdEdqUZ/ujPAOiMU/wlxeCa4lvwohKC6rJjTmz/jelEBGi9vBs9+gsGzHscvJNSpe9RfbefMrgqunWvC3V1N5thYsqbEExB259Qlh9lM+6bNNC9fjrWyEo/kZKL/9bcEzJ2L+jZFjNVhZee1neQU53C57TLRvtH849B/ZH7a/K4eMcf+G2rPgG8ETP4VDFlKo9WbnOPX+fBkMVqTjZEpofzbkwMYlxYmFTGSr4SzBf5PwEQhxGUAlUqVCmwHZIGX3BVrowHtoWoMhY0A9xxgLRwOLhecIm/zZ9RduoB3QCBjnnmRgVNn4eV397F7IQSVJS2c2VVB7aU2PH3cGTIziQETuzf/sut0tH3yCc25udhvNOHVvz8Rf/5v/CdP/rvUpPWX1rOqdBX1+np6BfXid2N+x4zkGWjUGrCaID8Hjv8PtFyB4GSY/Z8w8DmutNl4f/tVNhbWYHM4mNk/mu+MS2FAXJBLf08SibMFvvGL4t7JVaCxB/YjeYSwVGnRHqzCWNqMyl2N34ho/MbG4h7s2oj8FzmneZs/o6W2msCISCa//H0yJ0xG43H3JqPD7uByQSNndilxeH7Bnox+shcZY75E6tjURMuq1bR+/DEOrRbfUaMI/eMf8Rk+vMvT9BepSZ+Uf0KHpYPBkYP55YhfMjZ2rLLO2Ab5y+Hku6BvhOgsWLgS+s6loKqD9z4uZk9ZAx5uSrjGK2OTSQyVihjJveFsgS9RqVQ7gLUoZ/ALgTyVSjUfZHSf5BZCCMxX2tAerMZ8uQ2Vlzv+E+MVDbufa6ERFpOR8/t2k799I7rmJiXn9M2f0ttJV0erxU7ZsTqK9laibTYRHO1EHF5VFc0rVtC+fgPCasV/+nRCX3kF736ZXdZVaavILcll0+VNWOwWJsZPZGn/pQwMH6gs6KiFk/8H+SvBooXUSTD6R4iksRy4eIO/vn+KvOutBPloeGNSGotHJhLqJxUxkvuDswXeC2gAxnd+fQMIAeYgo/sk3PJh7zhQhbVKi9rfg8BZyfgOi0LdzdPx3TDpdBTu2sqZnVsxaTuIy+jHtFdfJylrsHOujgYr5w/WcO6AInWMSglk7FNpJPXvXqVjunCB5mUf0LFjR6fU8QlCli7BM7lrHF5Zcxk5xTnsqtiFWqVmbupcFmcuJiWwUxJ546LiEXP2UxB2yJynpCZF9GfbuTre/bPiERMb5M2v5yiKGB8PqYiR3F9kZJ/knhAOgfH8DbQHqrDWG3AL8SJoXi98B0Wi6mYQ6G7o21op2L6Jot07sJqMpAwayrAnniK2d1+nrjd0WDi7r5Lzh2qwmuwk9gtl0PREYtLufJYthMBYUEDTsmXoDx1G7eNDyEsvEbJ4cRep4xfmXznFORyvPY6vxpfFGYt5IeMFInw611WdVhQxF7YrqUmDX4KRP8Dol8Da/Crezz1ITZuR9Eg//vSU4hEjFTGSnsLZyL5k4A0g6fZrhBDdOkyqVCov4DDg2XnNZ0KIX9/LZiUPDsLmwFDYiPZQ53BShDfBT6XjMzAClZtrSo/2xgbytm6g+MBuHDY7vUeNZdjjTxKemHz3i4GOJiOFeyopO1aHw+5QXB2nJxLejdOkcDjQHTxE87JlGAsLcQsJIfxHPyT42WdxC7wVwWd32NlXuY/lxcu/xPxrl1LYK4+DVxCMexuGf4c2VQCrT1SQc3w/LXoLgxOD+Ze5mUzqEyFTkyQ9jrOfCTcBy4GtgMPJa8zAJCGETqVSaYCjKpVqpxDipAv7lDwgCKsdfV4D2kPV2NvNaGJ8CXm+L96ZoS4PJzVXV3F68zrKjh5EpVKTOWEyQ+cuIDgqxrnra3UU7qrkYl4DKhX0GRFF9rREgiK/xNVxxw6aP/gA86XLaGJiiPzlLwiaPx+19y15pNVuZdvVbawoXsH1jut3Nv8q+liJw2sshYA4mP57GPQidSY3lh+4xprT+Rgsdib1ieB7E1IZmiRTkyRfH84WeJMQ4s93X3YLoWQB6jq/1HT+c/d8QMkDicNsQ3+yHu2Rahw6Kx6JAQTN74XXPQRs1F+5xOlN67iUdwJ3Dw+yZ8xhyOx5+IeGOXV9w7UOCj6/zrWzTbh7qBkwMY6sKfH4daPScRiNtK3fQMuKFVhra/FMSyPmj38gYMYMVLcZjn0hdcwtyaXB0EDfkL78+/h/Z0rClK7mX8f/FzqqISID5r0H/RZwudnM+1uvsLGwBoeAOQOi+c74VPpGu6b1l0juBWczWZ8D0oDdKE/mAAghztzlOjcUi4NewF+EEP94hzWvAa8BJCQkDK6oqPgq+5f0MA6DMnWqPdY5dZoWRMDEeDySA10q7F8MJ53auJaKc4V4+viSPWM22TPn4hMQ6Nz1F1op2Flx0663/8Q4RcPejUrH3t5O65o1tKxajb21Fe9Bgwh97VUl5/S236Hd3M6a8jWsKVtDm7mNIZFDeKX/K4yKGdUpdWyF08vg5F/B2KKYf415C9KmUVTdzl8PXmZ3qSJ1fGZoPK+MTSE+xDW/eonEWb4sk9XZAv97YBHKJOsXRzRCCDHJyQ0EARuBN4QQxd2tk6HbDw52rQXt0Rr0J+oQFjtefUMImJTgcnKSEIJrhfmc2riW2otl+AQGMfixJxg4dZZzPuwOwbVzTRTsvE5jhRafQA+yJieQOa57Dbu1oYGWlbm0ffopDoMBv/HjCX3tVXwGD+6yrtHQyKqSVTddHSfETeDl/i+TFdGZKd9RByf/ogwoWXSQNh3G/hgRP5wjl5r468ErnLjaTICXO4tHJbF4VBJhUuoo+Zq4H6Hb84AUV/1nhBBtKpXqIDAD6LbAS755bG0mtIeq0ec1gN2B94BwAibGo4lybejG4bBz8eQxTm9ax42Ka/iHhTNp6XfpN3GqU8NJdruDy3kNFOyqpLVOT0CYF+Of602fkVG4a+6sgTdfu0bz8uW0b94CDgcBs2YR+srLePXu3WVdRUcFOcU5bLmyBYdwMCN5Bkv7LSU9OF1Z0HxFsRI4+zE4bJA5H8a8hT0ikx3n63h301FKajuIDPDkn2b15dnhCdL8S/JA4ex/jWeBIL7C9KpKpQoHrJ3F3RuYAvzbV9+i5OvA2mREe7AKw5lOO4FBEfhPiEfTjSfL3bDbrJQePkDels9oraslOCaOGd9/iz6jx+Pmfvf/7GwWO2XH6yjcXYm2xURIjC9TX86g16AI1N3ICk3l5TS99x7az3eh8vAgeOFCQpYuwSMursu68pZyPjj/AXsq9uCucmd+2nwWZy4m3j9eWVB3Do7+J5RuArUGsl+AUW9i8k9g/Zlq3l99kIpmAylhvvxhwQAez47B0901v3qJpCdxtsBHAuUqlSqPrmfwXxbEHQ3kdp7Dq4G1QohtLu9U0iNY6/V0HKjCeO4GuKnxHR6F//g43INcsxOwmk2c37+bvK0b0DU3EZGcypwf/5xeQ0c4FbBhNtooPlTN2X1fDCcFMPaZdJL6da/SMZwppPm999AdOoTa15fQV14hZPGLuIfdatYKIShoKOCD4g84VnMMX40vL2W+xKKMRYR5d66rOA5H/gSX94CHP4x6A0Z8nw5NKB+erGDF0QM06cwMjAvk5y8MYmpGFG5S6ih5gHG2wH9l/boQ4hyQfdeFkm8ES5WWjgNVStaphxt+4+LwHxOLWzdmW3fDpNdxdvcOCrZvwqjtIK5vP6a/9gaJAwc51Yw1dFg4t7+K84dqsBhtxGeEMHhG4pf7sB8/TvO772HIy8MtKEjRsD/3HG4BAV3WHa4+zAfnP6DoRhEhXiG8mf0mT/d5+paG/cLnyhN71UnwCYVJv4Chr9Jo9WLF0et8dPIsWrONsWlhfG98FiNTQ6Wro+ShwNlJ1kM9vRHJ14O5ogPt/kpMF1pRebsTMEXJOlX7uJZ1atR2ULB9M4Wfb8ViNJCcPYRhTywkrk/m3S8GtC0mZTjpaC02m4PUrHAGzUgkIvHOskLhcKDbv5+m997HdP487pGRRP78ZwQtXIj6tmatzWFj1/VdLC9ezqXWS0T7RvPzYT9nXto8vN29wW6Dc+uUwt5YAoHxMPMPkL2I6x2C93ZeZX1B9U1Xx++NT6Vf7N1VPhLJg4Szk6xabmnYPVA07XohhBT3PiSYr7XTsa8S8+U21L7uBMxIwm9kNGoXm4L6tlbyt23k7O4dWM0m0oaPYvi8p4lMTnXq+rZGA2d2VXDhRD0A6cMjGTQ9keBumrnCZqNjxw6a3n8fy+UraOLjifrNvxD4xBNdfNjNdjObL28mpziHal01KYEpvDPmHWYmz7xl15u3XBlOar0OYb3hiXeh/5OUNhj5v8/K2XG+Dne1mieHxPHa2BSSZM6p5CHF2Sf4Lto4lUr1BDCsR3YkuW8IITBfbUe7rxLz1XbUfhrFAGxENGoP15qC2pYm8ras5/zeXdhtNnqPGsvweU8RFp/o1PUttXoKPr/OpbwG1G5qMsfG8P/bu++4Ksv3geOfm733EmSIGxVQcI809yptmJaVZWpl3/au76/dt3KU5ij31izNNHPnStzgAlyALJUpe3Pu3x/PMaVAiAQE7/frxQt4zvOcc/Gcw8XDfe77ugIHeGHjeIsGGz9vIG3BAooTErTFSVOnYjNoIOKmN2tzinJYe34tyyOWk5qfSjundrze8XX6ePbBQBhAQRYcmwUH52jlej2CYMBn0HIIx+MzmL38BL+fTcbK1IgJvXwZ370JLjbVex9CUe4U1bp8k1JuEEK8fbuDUW4PKSWFFzPI2hVH0aUsrbLjMF+tsmM1E3tmchJHN/7Emd070Ol0+PW8l04jHq5yS7yUuGyOb7lE1IkUjIwNCOjrSWB/Lyxty58qqcvN5doPa0lfvJiSlBTM/P1x9DZr+wAAIABJREFUffcdrHr3RhjcmEWTXpDOiogVrDm3huyibLo06sIXPb+gk1snbZw8JwUOz4UjC6AwE3x7Q48FSJ+e7L+Yxuz5hzkck469hTGv9W/BE119sK3mcJWi3GmqOkTzwE3fGgDBqLIDdxwpJQXnr5G9K46iuGwMbU2wu78plsFu1a7seO1KIoc3/Ejk/t2AoG2ffnS6/yFsXdyqdPzV6EyOb7nEpdNpmJgZEjTIm4C+nrdcdZq+YgXXli2nNDMTiy5dcP/qSyy6dCnzxmZSbhJLwpfw0/mfKCwtpK9XX8a3G09bp7baDhlxWtek0OVQUgCth0OPV9A1as/2iKvMnh3C6cRM3Gy0BtZjOqlyvUrDU9VX9PCbvi4BLgH33/ZolGqRUlIQmU7W73EUJ+RgaGeqlewNckVU0NCiMmkJcRxa/wPnQvZjaGREQP8hdLzvwSrViZFScvlCBsd+u0TC2WuYWhrR+b4mtOvdGNMKro5LUlJIX7qUa6tWa6tO+/TBadJEzAMDy+yXkJ3AojOL2HBxAzqpY6jvUMa3HY+vnb4Oe+oFbarj6bXa9/6joftLFDs0Y+OJy8z9YR8Xk3PwdrTgiwfaMbKDh5rDrjRYqh58PSZ1koKINC2xX87F0MEM+webY9HepdqJPflSNIfX/8D5IyEYm5gSNGwEwcNGYmlnX3k8UhIfkc6xLZe4cjETcxsTuj3Q7NblBBITSVu4kIyf1iFLSrAZPBjHiRP+tuo0OjOahacXsjl6MwbCgJHNRvJU26dobK1fxHTlFOyfBhG/aHXYO06Abi9QYNGIH4/F891erQ57KzdrZo5pz5C2bhipOuxKA1fVIZqvgE+BfGArEAC8LKVcUYOxKRWQOkn+mVSyf4+j+GoeRo5m2D/cAotAZ0Q1k9aVi+c4tP4Hoo8fwcTcnM4jHqbDkPurVgDsL3VirOxN6flIC/y6N8KogjH/wugY0ubNI/PXX0EI7Ebcj+P48Zj4+JTZ72z6Weafms+O2B2YGpryaOtHedLvSVwtXbUd4o/C/qlwfqu2OKnHK9DlebKN7Fh5OI4F+7XFSe297Pj4fq0Ou5rDrtwtqjpEM0BK+aYQYiSQgNaTdTegEnwtkjpJ/qkUsn6PpyQ5DyNncxweaYm5v3O1m2wknA3n0Lo1xJ4Kw8zSim4PP0b7QcMxs7Kq9FidThIVmszxLZdIS9TqxPQZ24qWXdwq7HVaeOECqXO/I2vLFoSpKfaPjsHxqacwbtSozH4nU04y/9R89ibsxcrYimfaPcNYv7E4mDloi5Ni9sG+Kdpnc3vo8x50mkC6zpIlB2JYEhJKVoG2OOn53u3p4uugErty16lqgr8+cDoEWC2lTFe/LLVHa4uXStauWEqS8zFytcBhTCvMb9Fb9Jb3JyXx4ac5tG418RGnMbexpeej4wgcMAQT88orO5aW6rhwJInjW2PJSMrD3s2Cfk/50Tz4FnViIiNJnfsd2du3Y2BhgeMz43EYNw4jR8cycR29epR5p+dx+MphbE1teSHwBca0HlO2c9K+qZBwBKxcYcCnEPQUVwuMmL8rmlWH48gvLmVgG1ee792MAM/y2/Qpyt2gqgl+kxDiLNoQzfP6QmIFNReWAvrEHp5K1s44SpLyMHKxwOHRVpi3/XeJ/eBPq0iIPIOlvQO9n3gG/76DMDarfM53abGOs4euELotlqzUAhwbWzFwQluatneuMJ7806dJnTOXnN27MbCywvG5Z3F44gmM7G+M6Usp2Z+4n/mn5nMi5QRO5k68Hvw6D7d4GAtjC9DpIHyDNsZ+9ZS26nTIVG3VaWYp32+OYt3xREql5P4Ad57t3ZQWrtUra6woDUmV6sEDCCHsgSwpZakQwgKwkVJevZ3BqHrwGiklBeFpZO2Mo/hqLkbO5tj088K8XcWJtLL7+2ti73T/w/j3HYiRSeW1Z0qKS4n44zKh2+LIzSjEtYkNwYN98G5XcU2WvNBQUufMJfePPzCwtcXhySdwGDu2TJ0YndSxK24X80/NJzI9kkaWjXi67dOMbD5S3xKvBM78pM2KST0HDk2h52vgP4qzKfnM3RPFppOXMTI0YFRwYyb1aqoabCh3ndtRDx6gNeAjhLj5mGX/KjKlDCklBRHpZO2MpfhKLkZO+jH2gNuX2PuMm1T1xF5USvgflwnbFktuZhGNmtnS98nWNG5Vfps+KSV5R46SOmcOeYcPa02sX3sV+zGPYmh1Y7l/ia6ELTFbWHh6IVGZUXjbePNxt48Z5jsMY0NjKCmEY4u0JtYZseDaFh5aBH4jOJGYzawVJ9kZmYSliSETevoyvodadaoo5anqLJrlQFPgBFCq3yxRCf62kFJScDZdu2JPzMHwz1kxLtV68/S2JPb9lwndFkteVhEeLezo/3QbPFqWP1VSSknugRBS584l//hxDJ2dcHn7LexHjSpTAKyotIiNURtZeHohCTkJNLNrxle9vmKA94AbvU6PzNcWKGVf1soJDP4SWgziaOw1Zi4+xv4LqdhZGPNKvxY82c0bO4vqVb9UlLtBVa/ggwE/WdXxHKVKrq88zdoRqy1QcjDD/qEW2jz2OkjsxUWlhO9LJGx7nJbYW9ox4Jk2eLSoOLHn7NlD6tzvKDh1CiM3N1zffx+7hx7E4KYx/cLSQn46/xOLzywmKS+JNo5teKPjG/T27K2vE5MJRxfAwdmQlwY+PWHEHGSTewiJTmfmvEMcjknHycqEtwe3YmwXb9U5SVGqoKq/JWcAN+BKDcZy15BSUnghg6wdsRTFZ2NoZ6otUOrgUq157H9N7Fb2Dtz71CTa3VvFxF5Yypl9iYTtiCM/qwiPlvYMnNAG9+YVJHadjuydO0n97jsKIyIx9vDA7aOPsB1ZtrJjfkn+n4k9JT+FDi4d+Ljbx3R176oN8eSlaw2sD3+v1Ylp1h96vY707Mye8yl8+91BQuMycLE25b/D/Hi0kxfm1ayloyh3o6omeCcgQghxhKp3dFLKURCVQdb2WIpiszC0/XclBW5LYt+bSNiOWPKzi2ncyp6OE9vi3qz8qYWytJTsbdtInfsdhRcuYOLtTaPPP8d2+DCE8Y0SBHnFefx4/kcWn1lMWkEaHd068mWvL+no1lHbITcNDs6CI/O0Jtath0PP19C5BbIjMolZsw5wOjETDztzPhnRloeDGmNWQf9VRVEqVtUE/2FNBnE3KIrPJnP7JQovZGBgoy8C1tGtThJ7UUEJZ/YmcmJnHPnZxXi2tqfj0CY0ukViz/ptC6lz51IUHY1J06a4T5mCzeBBZUr25hXnsebcGpaGLyW9IJ3OjToz1X8qwW76N/hzUuDgt1plx+I8aDMSer1BqXNrtpy5wqy1+zl7NRtvRwu+etCfEe09MKlmyQVFUf5BRychhCugvwTjiJSyyg2472bFV3PJ3B5LQUQaBhZG2A5tglWXRohqXpEmnA0n5IcVxEecrlZiP70ngRM74ynIKcbLz4GOw5rg5lt+OYI/E/ucORTFxGDaogUe33yN9YABZUr25hbnsvrsapaGLyWjMINu7t14NuBZ2rvoOzZmJ2kNNo4uhNJCaPsQ9HqdEofmbDx5mdkr9hKVkktTZ0u+fiSA4f7uqk6MotwGVZ1FMwqYAuwBBPCtEOINKeVPNRhbvVaSlk/WzjjyTiQjTAy11ng9PDCooOhWZa5GXeDA2hVcOnEcC1s7+oybiH/fQf8sse+IpyC3GK82DnQcWkli37JVS+zR0Zg2b47HjBlY9+9XJrFnF2WzKnIVyyOXk1mYSU+PnkwKmESAc4C2Q9YVOPANHF8CpcXgPwp6vkaRXVN+DktgzpK9xKbl0crNmtmPdmBQW9XEWlFup6pmm/eAjtev2vUrWXcCKsH/RWlmIVm/x5F7NAlhKLRm1r0aY2hZvSYSKbExHFi7kqhjhzCztqHX2KcJHDAEY9PK530XF5Zyek8CYdvjKMgtxrutI8FDfXBrcovEvnUrqXPmUhQVpSX2b77BekD/Mok9qyiLlRErWR65nOyibHo37s2kgEk3arFnJmhz2EOXgSyFgNHQ41UKbHzKVHb0b2zLvMeD6NfaFQOV2BXltqtqgjf4y5BMGlrjD0WvNKeI7D0J5By6DBIsO7th08cLQ5vqzdNOS4zn4I+rOHdwP6YWlnQfNZYOQ+6rUq2YkuJSwvdd5vi2WPKzivBq40CnYb64Nqm4kXX21q2kzJlD0cUoTJs3K3coJrMwk+URy1kZuZKc4hzu9byXSQGT8HP003a4Fqs1sQ7T16ALfBR6vkq+pScrD8cyb99ukrMLCfK257ORbbmnhbMqAKYoNaiqCX6rEGIbsFr//SPAbzUTUv2iKyghe38iOfsTkcWlWHRwxaavF0YO1VtZmXH1CgfXrSZy/x6MTE3p8sAjBA0dWaXqjqUlOiIPXObYllhyMwrxaGlP54ltK37zVKcje9s2UmbPpuhiFCbNmuLx9XSsBw4sk9gzCjJYFrGMVWdXkVucS3/v/kz0n0grh1baDukxWp2Yk6tBGECHJ6DHy+RZuLPiUCzz9v1Oak4RXX0d+eaRQLo2rbjEgaIot88tE7wQohngKqV8Q9+2rwfaGPxBYGUtxHfHkiU6cg5eJnt3PLq8EszbOWHT3xtjl+rVQslKTebQ+h8I37MTAwNDgoaNoON9D1apHntpqY5zh65ybPMlstMLaNTUln5P+dG4opWnOh3Z27eTOns2hRcuYtK0KR7Tp2E9aNDfEvuS8CWsPrua/JJ8BvgMYKL/RFrYt9B2SIvSJ/Y1YGAEweOh+0vkmbuy4lAs3+/dTVpuET2bO/Fi3+Z09HGo1rlRFKV6KruC/wZ4F0BKuR5YDyCECNbfNrziQxsmqZPknUwha9slSjMKMW1uh+1AH0waV696Yc61dI5s+JFTO7cAENB/CJ1GPIyVfeXJUKeTXDhylSObL5GVko+LtzW9H2uJp1/5tc+1xL5Dn9gvaNMdp03FZtAghOGNWT2ZhZksi1jGysiV5BXnMchnEBP9J9LMvpm2Q+oFrWTv6bVgaAKdJ2mJ3dTpb4n95X7NCfJWiV1R6kJlCd5HSnnqrxullMeEED41EtEdrOD8NTK3xFB8JRdjd0vsH2yOWQWrPSu9r9wcjm5cR+hvGyktKaZtn/50eeARbJxcKj1W6iQXQ5M5+msM167m4eRpxZDn/fGpoLqjlJLsHTtInTWbwvPnMfH1xX3qVG0e+02JPbsomxWRK1gevpzs4mwGeA/guYDnbiT2tCjY+5WW2I3MoOtk6Pof8kwdWX4wlnn7TqvErih3kMoS/K0Gks1vZyB3sqLEHDK3xFB4MQNDBzMcRuu7KFVj5kdxUSFhWzZx9JefKMjNoVX3e+g26jHs3dwrPVZKScyJVI78Gk1aYi4O7pYMmtQW3wqqTUopyd23j5QZMymIiMCkSRNtgdKQwWUSe25xLqsiV7EkfAlZRVn09erLcwHP0dJB3xc1PUbrnnRyjXbF3vUF6PYieSb2+sR+SiV2RbkDVZbgjwohJkgp59+8UQgxHjh+qwOFEJ5o1SbdAB0wT0o5498EW9tK0gvI3HaJ/JMp2iKlYb7aIqVqrK7UlZZyZs8ODv60mpz0NJoEBtFjzJO4+PhWeqyUkrjwdA5vjCYlLhs7Vwv6j/ejWVDF0wtzDx0mZcYM8sPCMG7cmEZf/A/b4cPLJPbrK08Xn1lMRmEGvRv35rnA527MismI0xL7iVXaGHvnZ/VDMdev2E+QlltErxbOvNS3OUHe1ftvRlGUmlFZgn8Z+FkI8Rg3EnowYAKMrOTYEuA1KWWoEMIaOC6E2CGljPhXEdeC0txisn+PI+fQFYSBwLqPJ9b3NK7WIiUpJRcOH+CPNcu5diWRRs1bMuQ/r+Pp165Kx1+JyuTQhiguX8jAxsmMvk+2pkUn1wpb4+WfOEHyjBnkHTyEkasrbh9+iN2DD5SpFZNfks/ac2tZdGYR6QXp9PDoweTAyWXnse+fBqHLQQjtzdMer5Br6szyQ9oVe7pK7Ipyx7tlxpJSJgHdhBB9AP1vP5ullL9XdsdSyivoq09KKbOFEJGAB3DHJnhZoiMn5DJZv8chC0uxDHbDpp8Xhram1bq/2NMn2L9qKUnRF3Bs7MX9r79P0+DOVZoimJaYw6Fforl0KhULGxN6jW6BXw/3CptZF0REkDJjJjl792Lo4IDrO29jN3o0BqY3Yr9etnfB6QWk5qfStVFXng98nkCXQG2HrMta96TQpVr/0w5PQM/XyDVz1Sf23SqxK0o9UuWWff/qQbQ3ZPcBbaWUWX+5bSIwEcDLyysoNja2xuP5K62TUhqZv8VQklaAWUt7bIc0wdjVsvKDy5EUfZH9q5cSeyoMa0dnuo16DL9efTAwqLz+TGZKPkd+jeb8kSRMzIzoMNAL/z6eGJuWf2xhVBQpM78le9s2DGxscBw/Hoexj2FgeSP2otIi1l9Yz/zT80nOS6ajW0cmB04myDVI2yE7SVugdGyRtvK0/Vjo+Rr5Fh4sO3iJ7/dFq8SuKHeoW7Xsq/EEL4SwAvYCn+mnWlaoLnqyFl3OIfPXaAqjMzFyMcduqC9mLav3JmFm8lX2r1rKuYP7MbO2ocvIUQT0H1KlejF5WUUc++0S4fsTEQaCgHsb036AN2YVlDgoio8nddYsMjf9ioGZGQ7jnsRh3LgyPU9LdCVsitrE3JNzuZJ7hQ4uHZgcOJlOjTppO+SkaLViji6E0iIIHAO93qDAypPVR+KYvTuK1JxC/ZunLVRiV5Q70O3qyVqdBzYG1gErK0vuta00u4jMbZfIO56EgbmRVr63U6NqdVIqyMnh0M8/cGLrJoSBIZ1HPkLH+x7A1KLy/wAK80sI2x7LyV3xlJZI/Lo3InhIE6zsyx8WKk5KInX2HDLWr0cYGuIwbhyOE57ByP5G8pVSsiN2B7NOzCImM4Y2jm34sOuHNxpt5KZp1R2PzIOSAvB/BHq9QZFtE348Hs+s3/dwJbOALr4OfDe2A8FqgZKi1Es1luCFNtC8EIiUUk6vqcf5p2Sxjuw/EsneHY8s1WHV3QObez0xsPjnxcBKS4o5se03Dq1bTUFeLm3u6Uv3R8Zi7eBU6bElRaWc2pNA6LZYCnNLaB7sQqfhvti5lr8StjQzk7QFC0hfthyp02E/ahSOz07C2OXGvHkpJSGXQ5gZNpOItAia2jblm97fcK/XvVpiL8iCQ3MgZJbWaKPdQ3DPW5TYN+XnsERm/r6H+PR8OnjZMe3hALo1q/znUBTlzlWTV/DdgceB00KIE/pt70op66yGTf65dDI2RlGaVoCZn6M2zu70z6fzSym5cCSE/SuXkJF0Ba92gdwz9ukqTXnU6STnDl3l8MZocjMK8WrjSJf7fXH2Kn8lrK6ggGsrVpA6fwG6rCxshg3D+cX/YOLpWWa/sOQwZoTO4HjScTysPPisx2cMbTJUa2ZdnK/1PN0/HfLTofV90Oc9dE4t2XTqMjOW7CM6NZe2HjZ8/FRbeqsiYIrSINRYgpdS/oFWt6bOlaQXkPFrNAURaRg5m+M0vm21V6BePn+WvcsXcvl8JI6NvXjgnY/wCehQpYQYF5FGyLoo0hJzcPGxof/TfhU3tC4pIXPDBlK+nUVJUhKWvXri8uqrmLVqVWa/c+nnmBk2k30J+3A0c+Tdzu/yUPOHMDY01mqwH1uqrT7NvgJN74V7/4t0b8+28CS+Xrmfc0nZtHS15vvHgxjg56oSu6I0IA26Nb0s1pG9L4Gs3fEIATaDfLDu4VGthUpZKcnsXbmY8wf3Y2FrR/+JL9C2d38MDCufGZOakEPI+ovER6Rj42TGgGfa0CzIpeKyAjt3kvL1NxRFR2MW4I/7lK+w7NSpzH6xWbHMDpvNlktbsDGx4eUOLzOm1RgsjC1Ap4NTP8Luz+BaDHh2hgfmI316sOdcCtNm/cGZxCx8nS2ZOaY9w9o1UvXYFaUBarAJ/ubhGPN2TtgO9cXI7p/PZy8uLODoxnUc/WUdCEGXB0fT8b4HMTGrfGgn51ohhzdFc/bgFUzNjej+UDPa3dMYQ+Py/8DkHjlCyrTp5J88iYmvLx7fzsS6X78yfwiu5l7lu5PfseHiBkwMTZjQbgLj2o7DxsRGm7t+9jf4/VNIDgfXdvDoWmg+gJDoNKbODSE0LgNPB3OmPhzAiEDVGk9RGrIGl+BLc4rI2BRN/smUfzUcI6Xk/KE/2Lt8EdlpKbTs2pNeY5+qUjGwooISQrfFcnJnPDopCezrSdBgnwqnPBacO0fytGnk7tuvrT795GPsRo4s09A6qyiLBacXsDJiJRLJmFZjGN9uPE7m+jdCY/bBro8h4Sg4NIWHFoHfSE5fzuarRUfYfyGVRrZmfD6yHQ8HN8ZYJXZFafAaTIKXUpIXmkzm5mh0haXY9PPCurdntYZjki9Fs3vpPBIizuDs3YQhL7xGY7+2lR6nK9URceAKRzZFk59dTPNgF7qMaIpNBW/kFiclkzJzBpnrf8bA2hqX11/DfuxYDMxu1HgrKi1i9dnVzD89n6zCLIY3Hc7kwMm4W+mLkyUe1xJ79B6w8YDhMyHwMaLTC5i25gSbT13B3sKY94e2ZmwXb8yq2exbUZT6p0Ek+JK0fK79fJHCixmYeNtg/2DzajXeyMvKJGTtCk7t3IaplRX9nplMu74DqrQCNf5sOn+svUD65Vzcm9sxdHIzXH3Kb5Gny8sjbdFi0hYuRJaU4PDEEzg99yyGdjc6L+mkji0xW/g27FsScxLp7t6dV4JeuVHhMS1KS+wRG8DCCQb+D4Kf5moezPglkrXH4jE1MuDFvs2Z0LMJ1mbV6wmrKEr9Ve8TvC6vmKSZYQDYjdAvVvqHbxhKnY7Tv29n/6olFObnEThoKN0eeqxKbfIyU/IJWXeR6BMp2DiZaeV7A8ufZihLS8nc8AspM2ZQkpyM9YABuLz+GiZeXmX2O3TlENOPTScyPZJWDq34vv/3dHPvpt2YkwJ7v4Tji8HQFO55G7q9QGapGXN3RrH4QAw6KXm8izeT+zTD2bp6dXQURan/6n2CN7Awxm5EM0x9bTGqRlGw5EvR7Fw4hyvnz9LYry19n34OJ0/vSo8rKijh+JZYTuyKw8DQgC4jfAno64lRBUMguYcOkfTlVxRGRmIW4I/HN19j0aFDmX3OpZ/j69CvOZB4AHdLd/7X838MaTIEA2EARblwcDYcmKHNaw96Eu55m3xTJxaHxPDdniiyC0sYGejBK/1b4OlQvdaBiqI0HPU+wQNYtq/8jc+/KsrPI+THVYRu2YiZlTWDJ79K6559Kp0HLnWSc4evcvDnKPKyimjZxY2uI5piWcEMncKoKJKnTCVnzx6M3d21FnlDhvxtZsy3Yd+yKWoT1ibWvB78OqNbjcbU0BRKSyBsCez5AnKSoPVw6PsBxfZNWXssnhk7d5OcXUjfVi68PrAlrRuVPyykKMrdp0Ek+H/i+irU3UvmkZOehn+/QfQY8yTmVpX3VE2KyWLfmnMkx2bj4mPD4Ofa4dak/KbYpRkZpHw7i2tr1mBgbq69gfr442XK9+YV57HozCKWhi9FJ3WMazOO8e3GY2tqq015jPwVdn0EqefBswuMWo707MSWM1eZsmQfMam5BHvbM/uxDqqhtaIof3NXJfica+nsXDCHqGOHcPbyYfgrb+PeonWlxxXkFnNoQxThf1zGwsaEfuNa06KTW/lt8kpLyfjxR1K+mUFpVhb2ox/B6YUXMHK4kYB1UsemqE3MDJ1Jcn4yg5sM5uUOL9+YGRN/BLa/D/GHwakFjF4FLYdwPC6Dz/Rz2Vu6WrNoXDB9Wpa/YEpRFOWuSPBSSsL37mLPsvmUFhXT67GnCBo6otJVqFJKzh68Ssj6ixTmFhPQx5NOw5tgYl7+acs7doyrn31OYWQkFh074vr+e5i1bFlmn9CkUL46+hXhaeG0c2rHtN7TbjTcyIiDHR9A+HqwcoVh30D7x4nNKOTLVaH8dvoqLtamfPlgOx4K8sRQrT5VFOUWGnyCz0pNZsf82Vw6cRyPVn4MmPQSDu4elR6XlpjD3tXnuHIxEzdfG3qNCcTZs/xhnOKrV0meMpWszZsxatQIj6+nYz1oUJkr68ScRL4+/jXbLm3DxcKFz3t8zlDfodobqIXZWsONkFkgDKDXm9D9Ja6VmPDtb+dZfugSxoYGvNKvBRN6NcHCpME/bYqi3AYNNlNIKQnfs5PdS+chdZJ7n5pE4IChCINbL3wqKijh6OZLnNwVj6m5EX0eb0XrruVPvdQVFpK+eAmp338PpaU4Pf8cjs88g4HFjRksucW5LDi9gGXhyzAQBjwf8DxPtnlSXzOmFMKWw++faG+gthsF/T6gwKIRyw5e4tvfL5JbWMIjHT15pV8LXGzM/haDoihKRRpkgs/PyWbnvFmcP3wAT792DHzuJWxd3Co9Lj4ind0rz5KdVoBfD3e6jmiKmVX5C4Ry/jjA1Y8/pjguDuv+/XB56y1MGjf+83YpJb9G/8r049NJzU9lmO8wXurwEm6W+jhi9sO2d+DqaWjcCUavQucexKZTl5mybS8J1/Lp09KZd4a0poVr5W8AK4qi/FWDS/Dx4af4bfZ08jKu0fPRcQQPH1npStSC3GIOrLvI2ZAr2LlaMPK1Drg3tyt33+LkZJK/+IKs37Zg4uOD58IFWHXvXmafc+nn+Pzw54Qmh9LOqR0z+szA39lfuzEtCnb8H5z9FWw94cGF0PZBwuIz+GhuCCfiM/BrZMPKZ/zprhpuKIryLzSYBC91Og6t/4GQn1Zh7+bOo59Ow9W3WaXHRYUms3fNeQpyiukwyJuOQ33KXawkS0u59sMPpEz/GllUhNN/XsBxwgQMbuq3mlWUxZwTc1hzdg3WJtZ82PVDRjYfqR9nz4H9U7VxdiNTuPe/0HUySfmCL9eeZH1YIi7Wpkx5yJ8HOzRW5XuZ/14jAAATwElEQVQVRfnXGkSCL8jJYcvsaUSHHsWvZx/6PTMZY7Nbj1fnZxexd9U5osJScPK0YvgLARV2VcoPD+fqhx9RcPo0lt264vZ//4eJj8+ft0sp2RS9iWnHpnGt4BqjWo7iP+3/c2M++5n12rTHrEQIeFQbZzdzZuEfMczefZGSUsnzvZvyfJ9mWJk2iKdEUZQ7QL3PJvnZWax871WyU1Pp+/RzBAwYUum88EunU/l9+VkK84rpMsKXwP5eGJZTPleXn0/KjJmkL1uGoYMD7lOnYjO07P2fSz/HZ4c/Iyw5DH9nf+b2m4ufo592Y3Ik/PYGXNoPbv7w0GKkZye2hSfx2W97iU/PZ2AbV94b4oeXoyotoCjK7VXvE7yZlTUtuvSgaVBnPFreetFScWEpB9ZdJHxfIo4eltz3YiBOjcsvKJZ75AhX3v8vxXFx2I1+BJdXX8XQ5kYZgPySfL47+R1Lw5diY2LDx90+5v5m92vDMQVZWkGww9+BiRUMnQZBT3E2OZePFxwmJCqNlq7WrHymsxpnVxSlxtT7BC+EoNej4yrdL+lSFjsWhZOZkk9gfy8639ek3LF2XW4uydOmc23VKow9PfFauhTLzmXb5YVcDuGTg5+QkJPAA80f4NWgV28Mx5xaqw3H5CRDhyeg7wdkGdow/dezLDt4CRtzYz65vw1jOnmpbkqKotSoep/gKyOl5PSeBA78dBELGxNGvNwej5bld3jKPXhQu2q/fBn7Jx7H5eWXy8xpTy9IZ+rRqWyK3oSPjQ+LBi6io1tH7ca0KPj1FYjZCx5BMGY10r0DG09e5tPNYaTmFPJYZy9eH9ASOwuTch9fURTldmrQCb4ov4Tfl58lKjQZn3aO9B3nV27bPF1BAclTpnJt5UpMfHzwXrmiTCnf62+iTjk6hZyiHCb6T2Si/0St2mNJEYTMgL1TtNkxQ6dB0NNcTM3lv/MPczA6jYDGtix8Mhj/xuVPvVQURakJDTbBX7uay+Y5p8hKLaDryKa07+9V7mrUgrNnSXz9dYouRuHw5BM4v/JKmZZ5yXnJfHTwI/Yl7CPAOYAPun5Ac/vm2o1xh2HTS5ASCX4jYPCX5Jk68e328yzYH425sSGfjmjLmE5eqm6Moii1rkEm+PjIdLbOO4OhkWDEK4G4l9N0W+p0pC9ZSsrXX2NoZ4fnggVY9bixYElKyeaYzfzv8P8oKi3irY5v8WjrR7U3UfMztDK+xxZpi5XG/AAtB7E9/CofbdpHYkY+DwU15u3BrXCyUh2VFEWpGw0uwYfvT2Tv6vPYu1kwdLI/No5/b3hdkpbG5TfeJDckBKt+fWn0yScY2d/4I5Can8qnhz5lV9wuApwD+LT7p/jY+mg3ntuqXbXnJkOXydDnXZILjfi/5cfZGn6VVm7W/PhsV1WfXVGUOtegEnzotlgO/hyFd1tHBoxvU25Z37zQMBJfeYXSjAzcPv4Iu4cfLjOvfVfcLj4K+Yic4hxeDXqVJ/yewNDAULtq3/oOnFwFLm3g0TXIRoGsPRbPZ5sjKSjR8eaglkzo6Yuxmh2jKModoMEk+KObYziyKYbmHV3pO6713xYuSSm5tnw5SV9NwdjdHZ81qzFrfWPefEFJAVOOTmHt+bW0dmjN/3r+j6Z2TbUbz2+HTS9qUx97vQG93iQ2s5h39HPaOzVx4IsH2uHrXHmTbkVRlNpSYwleCLEIGAYkSynb1tTjgDYsc2RTDK26uNHnidZ/q+OiKyzkyvv/JWvTJqz69sX9f5+XWbR04doF3tz3JhczLvJUm6f4T/v/YGxoDAWZsPVdOLECnFvB6FWUuAWy6EAM03ecx9jAgM9GtmVMRy9VO0ZRlDtOTV7BLwFmActq8DEoyCkmZN1FvNs60ufxVn9LtCVpaSS88B/yw8JwfvllHCdN/HNIRkrJj+d/5KujX2FlbMX3/b6nm0c37cC4Q7DuGa1+TI9XoffbRF0r5tXvDnIyPoN+rV35dERb3GxVjXZFUe5MNZbgpZT7hBA+NXX/15lZGTPitQ7YOptj8JdhmcKoKOInTqIkLQ2PGTOwGTjgz9vyS/L55OAnbIreRHf37nza41OczJ2gtESr+rj3S22GzNPb0XkEs+zgJb7YehYzY0NmjmnPcP9Gqheqoih3tDofgxdCTAQmAnh5eVXrPsprpZcfHk78+GfAyAjv5cswb9fuz9sSshN4Zc8rnEs/x/OBzzPJf5I2/TEjHtZPgLiDWnelodO4XGDMm4uO8MfFVHq3dObLB/1xVZ2VFEWpB+o8wUsp5wHzAIKDg+XtuM+8sDDiJ07CwNoK78WLMfH2/vO2kMshvLnvTXQ6HbP6zqJX417aDZGb4JfJWhu9kfMg4BE2hCXy31/OUKqTfD6yHWM6eaqrdkVR6o06T/C3W0FEBPHPTMDIyQmvxYswdnf/87Z159fxyaFPaGLbhBl9ZuBl46UNyfz+CRz4Btzbw0OLyLX04r9rT7A+NJEgb3umjwrA29GyDn8qRVGUf65BJfiiuDjiJkzEwNYGr6VLMHbT+p9KKfk27Fvmn55Pd/fuTL1nKlYmVpCbCj89rRUIC3oKBn9JRHIhLyz+g5jUXF7q25wX+zZXZQYURamXanKa5GqgN+AkhEgAPpBSLqypx9Pl5RH//PNQUoLX8mV/JvcSXQkfhHzAxqiNPNj8Qd7r8h7GBsaQGAo/PA65KXD/bGTgY6w4HMcnv0ZgZ27Mymc6062pqtWuKEr9VZOzaMbU1H2X5+qnn1EUFY3XwgWY+voCUKwr5t3977L10laeD3yeZ/2f1cbQIzfBuglg6Qzjt5Pn1Ja31pxg08nL3NPCmWmjAlQNGUVR6r0GMUSTe+QImevX4zhxIpbdtHnsxbpi3tz7JjvjdvJa0GuMaztOa8gRMktryKGv2R5fZMWEOSGcS8rmjYEtee6epmrRkqIoDUKDSPCpc+di5OaG03PPAtqY+4chH7IzbidvdXyLsX5jQaeDLW/C0fngdz+M/J4/LuXywuo/0Okki8d1pHdLlzr+SRRFUW6fep/gS3NyKY6Nw370IxiYa5Ujvw37lo1RG5kcOFlL7qUl2hTIU2ug6wvQ/xOWHY7jw43hNHOxYt7jwfg4qVkyiqI0LPU+wRtaWdJ0x3ZkcTEAv0X/xvzT83moxUNM8p+kdVxa/wxE/AJ93kfX4zW+3HqO7/dF06+1CzNGt8fStN6fBkVRlL9pEJlNGBoiDA2JyYzho4Mf0d6lPe92fhchdTeS+8DPKQh+ltd/OMGvp67wRFdvPhjeRk2BVBSlwWoQCR5AJ3W8/8f7mBia8FWvrzAWRlqJX31yzwuaxISlRzlwMY13BrdiYi9ftSpVUZQGrcEk+HUX1nEq9RSf9/gcN0s3+P1TCF0GPV8np8Mknl58lGOX0pk+KoAHOjSu63AVRVFqXINI8MWlxXx/8nvau7RnmO8wCN8A+6ZA+8fJ7vYW4xYd4UR8BjNGt2d4gHvld6goitIANIjecjvjdpKUl8RE/4mIlHOw4Xlo3InCgV8xaUUoJ+MzmDVGJXdFUe4uDeIKflfcLpzMnejm2hEW9gdjc3QPL+PV9WcJiUpj+qgABrdrVNdhKoqi1KoGkeAj0yIJcg3CIGQmXDkJo5bx5YEMNp+6wjuDW6kxd0VR7kr1fohGJ3Vczr2Mh4kd7J8Ore/jt9JOfL8vmrFdvJjYy7euQ1QURakT9T7BCwS7Ht7Fk6nJUFpMXNBbvPHjSdp72fF/w9qoqZCKoty16v0QjRACB4zg1E/o2j3My9szMTI0YM5jHTAxqvd/vxRFUaqtYWTAyI1QnMtm08GExmXwwXA/Gtma13VUiqIodareX8EDELUbnaUL7x8xpWdzO0a296jriBRFUepcw7iCTzpDlEkrMgtKeHtwKzXuriiKQgNJ8DIrkWMZlvT3c6WNu21dh6MoinJHqP8JXqcjsvWLbCgIZmwX77qORlEU5Y5R/8fgDQxYrhtMhMllujd1rOtoFEVR7hj1/woeOJWQQXtve4wMG8SPoyiKcls0iIx4JbMAT3s1LVJRFOVm9T7B63SSe1o4E+xjX9ehKIqi3FHq/Ri8gYHg60cC6zoMRVGUO069v4JXFEVRyqcSvKIoSgOlEryiKEoDVaMJXggxSAhxTghxUQjxdk0+lqIoilJWjSV4IYQhMBsYDPgBY4QQfjX1eIqiKEpZNXkF3wm4KKWMllIWAWuA+2vw8RRFUZSb1GSC9wDib/o+Qb+tDCHERCHEMSHEsZSUlBoMR1EU5e5Skwm+vJq98m8bpJwnpQyWUgY7OzvXYDiKoih3l5pc6JQAeN70fWPg8q0OOH78eKoQIraaj+cEpFbz2Jqk4vpnVFz/jIrrn2mIcVVYRldI+beL6ttCCGEEnAf6AonAUeBRKWV4DT3eMSllcE3c97+h4vpnVFz/jIrrn7nb4qqxK3gpZYkQ4gVgG2AILKqp5K4oiqL8XY3WopFS/gb8VpOPoSiKopSvIa1knVfXAVRAxfXPqLj+GRXXP3NXxVVjY/CKoihK3WpIV/CKoijKTVSCVxRFaaDqfYK/UwqaCSE8hRC7hRCRQohwIcRL+u0fCiEShRAn9B9D6iC2S0KI0/rHP6bf5iCE2CGEuKD/XKstsYQQLW86JyeEEFlCiJfr6nwJIRYJIZKFEGdu2lbuORKamfrX3CkhRIdajmuKEOKs/rF/FkLY6bf7CCHybzp339VyXBU+d0KId/Tn65wQYmAtx/XDTTFdEkKc0G+vlfN1i9xQ868vKWW9/UCbfhkF+AImwEnAr45iaQR00H9tjbYGwA/4EHi9js/TJcDpL9u+At7Wf/028GUdP49X0RZs1Mn5AnoBHYAzlZ0jYAiwBW21dhfgcC3HNQAw0n/95U1x+dy8Xx2cr3KfO/3vwUnAFGii/501rK24/nL7NOD/avN83SI31Pjrq75fwd8xBc2klFeklKH6r7OBSMqpvXMHuR9Yqv96KTCiDmPpC0RJKau7ivlfk1LuA9L/srmic3Q/sExqDgF2QohGtRWXlHK7lLJE/+0htFXitaqC81WR+4E1UspCKWUMcBHtd7dW4xJCCGAUsLomHvsWMVWUG2r89VXfE3yVCprVNiGED9AeOKzf9IL+X61FtT0UoieB7UKI40KIifptrlLKK6C9AAGXOojrutGU/aWr6/N1XUXn6E563T2NdrV3XRMhRJgQYq8QomcdxFPec3ennK+eQJKU8sJN22r1fP0lN9T466u+J/gqFTSrTUIIK2Ad8LKUMguYCzQFAoEraP8i1rbuUsoOaLX5JwshetVBDOUSQpgA9wE/6jfdCeerMnfE604I8R5QAqzUb7oCeEkp2wOvAquEEDa1GFJFz90dcb6AMZS9kKjV81VObqhw13K2Vet81fcE/48LmtUkIYQx2hO4Ukq5HkBKmSSlLJVS6oD51NC/prcipbys/5wM/KyPIen6v336z8m1HZfeYCBUSpmkj7HOz9dNKjpHdf66E0I8CQwDHpP6gVv9EEia/uvjaGPdLWorpls8d3fC+TICHgB+uL6tNs9XebmBWnh91fcEfxRoLoRoor8SHA1srItA9ON7C4FIKeX0m7bfPHY2Ejjz12NrOC5LIYT19a/R3qA7g3aentTv9iTwS23GdZMyV1V1fb7+oqJztBF4Qj/boQuQef1f7doghBgEvAXcJ6XMu2m7s9A6qSGE8AWaA9G1GFdFz91GYLQQwlQI0UQf15HaikuvH3BWSplwfUNtna+KcgO18fqq6XeQa/oD7R3n82h/fd+rwzh6oP0bdQo4of8YAiwHTuu3bwQa1XJcvmgzGE4C4dfPEeAI7AIu6D871ME5swDSANubttXJ+UL7I3MFKEa7ghpf0TlC+xd6tv41dxoIruW4LqKN0V5/nX2n3/dB/XN8EggFhtdyXBU+d8B7+vN1Dhhcm3Hpty8Bnv3LvrVyvm6RG2r89aVKFSiKojRQ9X2IRlEURamASvCKoigNlErwiqIoDZRK8IqiKA2USvCKoigNlErwyl1FCDFSCCGFEK1u432OEEL43a77U5TbRSV45W4zBvgDbVHc7TICrTqgotxR1Dx45a6hrwVyDugDbJRSttKvvvwBsEFrQv8cEIK28jAYbYHKIinl10KIpmgLUJyBPGAC4AD8CmTqPx4EhgLPotWJiZBS3s4/JopSZUZ1HYCi1KIRwFYp5XkhRLq+kUIfYJuU8jP9snULtGJZHlLKtgBC31ADrTHys1LKC0KIzsAcKeW9QoiNwK9Syp/0+78NNJFSFt50rKLUOpXglbvJGOAb/ddr9N9vAhbpi0FtkFKeEEJEA75CiG+BzWillq2AbsCPWmkRQGtgUZ5TwEohxAZgQ838KIpSOTVEo9wVhBCOaLVJktGGXQz1n73ROu4MBV4Epkgpl+kT+kBgHJACvAyck1L+rfGCEGIJZa/gDdE6C92HVnOkjbzRoENRao16k1W5WzyE1iXHW0rpI6X0BGLQEnGylHI+2rh7ByGEE2AgpVwH/Bet3VoWECOEeBj+7JsZoL/vbLRWbAghDABPKeVu4E3ADrCqvR9TUW5QQzTK3WIM8MVftq1DqzKYK4QoBnKAJ9C65yzWJ2uAd/SfHwPmCiHeB4zRhnlO6j/PF0K8iDY7Z6EQwhatKuDXUsqMGvupFOUW1BCNoihKA6WGaBRFURooleAVRVEaKJXgFUVRGiiV4BVFURooleAVRVEaKJXgFUVRGiiV4BVFURqo/wc4Yg6igEQSpgAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -269,8 +269,7 @@ } ], "source": [ - "ss = ks_ss()\n", - "plt.plot(ss['a_grid'][:10], ss['a'][:, :10].T)\n", + "plt.plot(ss[\"a_grid\"], ss.internal[\"household\"][\"c\"].T)\n", "plt.xlabel('Assets'), plt.ylabel('Consumption')\n", "plt.show()" ] @@ -287,19 +286,19 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 564 ms\n" + "Wall time: 775 ms\n" ] } ], "source": [ - "%time ss = sj.krusell_smith.ks_ss()" + "%time ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { @@ -311,19 +310,21 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 1.42 s\n" + "Wall time: 1.69 s\n" ] } ], "source": [ - "%time _ = sj.krusell_smith.ks_ss(nA=2000)" + "calibration_highA = {**calibration, **{\"nA\": 2000}}\n", + "\n", + "%time _ = ks_model.solve_steady_state(calibration_highA, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { @@ -347,33 +348,19 @@ "evaluated at the steady state. Every column can be interpreted as the impulse response to a one-period news shock.\n", "\n", "### 3.1 Simple blocks\n", - "To build intuition, let's start with the firm block. In our code, simple blocks are specified as regular Python functions with the added decorator ``@simple``. In the body of the function, we directly implement the corresponding equilibrium conditions. The decorator turns the function into an instance of ``SimpleBlock``, a class that, among other things, knows how to handle time displacements such as `K(-1)` to denote 1-period lags and `r(+1)` to denote 1-period leads. In general, one can write (-s) and (+s) to denote s-period lags and leads." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "@simple\n", - "def firm(K, L, Z, alpha, delta):\n", - " r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta\n", - " w = (1 - alpha) * Z * (K(-1) / L) ** alpha\n", - " Y = Z * K(-1) ** alpha * L ** (1 - alpha)\n", - " return r, w, Y" + "To build intuition, let's start with the firm block we instantiated above. In our code, simple blocks are specified as regular Python functions with the added decorator ``@simple``. In the body of the function, we directly implement the corresponding equilibrium conditions. The decorator turns the function into an instance of ``SimpleBlock``, a class that, among other things, knows how to handle time displacements such as `K(-1)` to denote 1-period lags and `r(+1)` to denote 1-period leads. In general, one can write (-s) and (+s) to denote s-period lags and leads." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Simple blocks can compute their Jacobians by using the method `SimpleBlock.jac` This takes in the steady state dict returned by `ks_ss` and two optional inputs: the truncation horizon and list of variables to differentiate with respect to. It returns the Jacobians in a nested dict, where the first level is the output variable $Y$ and the second level is the input variable $X$." + "Simple blocks can compute their Jacobians by using the `jacobian` method. This takes in the `SteadyStateDict` object returned by the `ks_model`'s `solve_steady_state` method and two optional inputs: the truncation horizon and list of variables to differentiate with respect to. It returns the Jacobians in a nested dict, where the first level is the output variable $Y$ and the second level is the input variable $X$." ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -389,7 +376,7 @@ } ], "source": [ - "J_firm = firm.jac(ss, T=5, shock_list=['K', 'Z'])\n", + "J_firm = firm.jacobian(ss, exogenous=['K', 'Z'], T=5)\n", "print(J_firm['Y']['Z']) # Jacobian of output Y vs. TFP Z" ] }, @@ -397,14 +384,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "By default, `SimpleBlock.jac` compoutes the Jacobian for each input-output pair. In practice, it only makes sense to do so with respect to endogenous variables and shocks, hence the `shock_list` option. In this model, capital and TFP are the only inputs that will ever change.\n", + "By default, `jacobian` compoutes the Jacobian for each input-output pair. In practice, it only makes sense to do so with respect to endogenous variables and shocks, hence the `exogenous` option. In this model, capital and TFP are the only inputs that will ever change.\n", "\n", - "The Jacobian is diagonal because the production function does not depend on leads or lags of productivity. Such sparsity is very common for simple blocks, and we wrote the SimpleBlock class to take full advantage of it. For example, if we leave the truncation parameter $T$ unspecified, which is recommended, `SimpleBlock.jac` returns a more efficient sparse representation of the Jacobian." + "The Jacobian is diagonal because the production function does not depend on leads or lags of productivity. Such sparsity is very common for simple blocks, and we wrote the SimpleBlock class to take full advantage of it. For example, if we leave the truncation parameter $T$ unspecified, which is recommended, `jacobian` returns a more efficient sparse representation of the Jacobian." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -416,7 +403,7 @@ } ], "source": [ - "J_firm_sparse = firm.jac(ss, shock_list=['K', 'Z'])\n", + "J_firm_sparse = firm.jacobian(ss, exogenous=['K', 'Z'])\n", "print(J_firm_sparse['Y']['Z'])" ] }, @@ -434,12 +421,12 @@ "### 3.2 HA blocks\n", "HA blocks have more complicated Jacobians, but they have a regular structure that we can exploit to calculate them very quickly. For comprehensive coverage of our **fake news algorithm**, please see the [het-agent Jacobian notebook](het_jacobian.ipynb) as well as the paper.\n", "\n", - "HetBlocks have a `HetBlock.jac` method that is analogous to `SimpleBlock.jac` above." + "A `HetBlock` object has a `jacobian` method that is analogous to one above for `SimpleBlock` objects." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 30, "metadata": { "scrolled": true }, @@ -457,7 +444,7 @@ } ], "source": [ - "J_ha = household.jac(ss, T=5, shock_list=['r', 'w'])\n", + "J_ha = household.jacobian(ss, exogenous=['r', 'w'], T=5)\n", "print(J_ha['C']['r'])" ] }, @@ -467,12 +454,12 @@ "source": [ "Notice that this matrix is no longer sparse. This generally the case for HA blocks. The Bellman equation implies that policies are forward-looking, and then aggregates are also backward-looking due to persistence coming via the distribution.\n", "\n", - "Fortunately, our `SimpleSparse` Jacobians play nicely with these full matrices, so that we can easily combine the Jacobians of simple blocks and HA blocks. For example, the multiplication operator `@` maps any combination of SimpleSparse and full matrices into full matrices. " + "Fortunately, our `SimpleSparse` Jacobian objects are conformable with standard `np.array` objects, so that we can easily combine the Jacobians of simple blocks and HA blocks. For example, the multiplication operator `@` maps any combination of SimpleSparse and `np.array` objects into `np.array` objects as in standard `np.array` matrix multiplication. " ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 31, "metadata": { "scrolled": false }, @@ -527,18 +514,22 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 48, "metadata": { "scrolled": true }, "outputs": [], "source": [ + "# Import the JacobianDict class, since we are manually constructing Jacobians to demonstrate how the automatic construction\n", + "# works in the code\n", + "from sequence_jacobian.jacobian.classes import JacobianDict\n", + "\n", "# firm Jacobian: r and w as functions of K and Z\n", - "J_firm = firm.jac(ss, shock_list=['K', 'Z'])\n", + "J_firm = firm.jacobian(ss, exogenous=['K', 'Z'])\n", "\n", "# household Jacobian: curlyK (called 'a' for assets by J_ha) as function of r and w\n", "T = 300\n", - "J_ha = household.jac(ss, T=T, shock_list=['r', 'w'])" + "J_ha = household.jacobian(ss, exogenous=['r', 'w'], T=T)" ] }, { @@ -551,7 +542,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 33, "metadata": {}, "outputs": [], "source": [ @@ -568,11 +559,12 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 41, "metadata": {}, "outputs": [], "source": [ - "J = {**J_firm, 'curlyK': {'K' : J_curlyK_K, 'Z' : J_curlyK_Z}}" + "J = copy.deepcopy(J_firm)\n", + "J.update(JacobianDict({'curlyK': {'K' : J_curlyK_K, 'Z' : J_curlyK_Z}}))" ] }, { @@ -587,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 42, "metadata": {}, "outputs": [], "source": [ @@ -604,7 +596,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 43, "metadata": {}, "outputs": [], "source": [ @@ -623,7 +615,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 44, "metadata": {}, "outputs": [], "source": [ @@ -643,7 +635,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 45, "metadata": {}, "outputs": [], "source": [ @@ -658,32 +650,20 @@ "\n", "These steps can be automatized for the entire class of SHADE models. Any SHADE model is characterized by its blocks, exogenous shocks, unknowns, and targets. The only other things we need to know are the steady state around which to linearize, and the truncation horizon.\n", "\n", - "If we define a \"market clearing\" block that returns the target (asset market clearing), we can get the general equilibrium Jacobians by simply calling `jacobian.get_G`. Note that `get_G` takes in the model blocks (in arbitrary order), the names of exogenous shocks, the names of unknown endogenous variables, the names of target equations, the truncation horizon, and the steady state dict." + "Using the market clearing block we instantiated earlier in conjunction with the firm and household blocks allows us to calculate the general equilibrium Jacobians by calling the `ks_model`'s `solve_jacobian` method. Note that this method takes in the `SteadyStateDict` object we solved for earlier along with the names of exogenous shocks, the names of unknown endogenous variables, the names of target equations, and the truncation horizon." ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 46, "metadata": {}, "outputs": [], "source": [ - "@simple\n", - "def mkt_clearing(K, A):\n", - " asset_mkt = A - K\n", - " return asset_mkt" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "G2 = sj.jacobian.get_G(block_list=[firm, mkt_clearing, household], # we could replace household with J_ha here\n", - " exogenous=['Z'],\n", - " unknowns=['K'],\n", - " targets=['asset_mkt'],\n", - " T=T, ss=ss)" + "exogenous = ['Z']\n", + "unknowns = ['K']\n", + "targets = ['asset_mkt']\n", + "\n", + "G2 = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T)" ] }, { @@ -695,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 47, "metadata": {}, "outputs": [], "source": [ @@ -715,7 +695,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 50, "metadata": { "scrolled": true }, @@ -737,6 +717,7 @@ "rhos = np.array([0.2, 0.4, 0.6, 0.8, 0.9])\n", "dZ = 0.01*ss['Z']*rhos**(np.arange(T)[:, np.newaxis]) # get T*5 matrix of dZ\n", "dr = G['r'] @ dZ\n", + "\n", "plt.plot(10000*dr[:50, :])\n", "plt.title(r'$r$ response to 1% $Z$ shocks with $\\rho=(0.2 ... 0.9)$')\n", "plt.ylabel(r'basis points deviation from ss')\n", @@ -753,7 +734,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 52, "metadata": { "scrolled": true }, @@ -762,7 +743,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 21.9 ms\n" + "Wall time: 16.9 ms\n" ] } ], @@ -787,7 +768,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 54, "metadata": { "scrolled": false }, @@ -808,6 +789,7 @@ "source": [ "dZ = 0.01*(np.arange(T)[:, np.newaxis] == np.array([5, 10, 15, 20, 25]))\n", "dK = G['K'] @ dZ\n", + "\n", "plt.plot(dK[:50])\n", "plt.title('$K$ response to 1% Z news shocks for $t=5,...,25$')\n", "plt.show()" @@ -868,7 +850,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 55, "metadata": {}, "outputs": [], "source": [ @@ -894,7 +876,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 56, "metadata": { "scrolled": false }, @@ -905,7 +887,7 @@ "(300, 4, 2)" ] }, - "execution_count": 27, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -926,7 +908,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 60, "metadata": { "scrolled": false }, @@ -941,8 +923,8 @@ ], "source": [ "sigmas = np.array([sigma_persist, sigma_trans])\n", - "Sigma = sj.estimation.all_covariances(dX, sigmas) # burn-in for jit\n", - "%time Sigma = sj.estimation.all_covariances(dX, sigmas)" + "Sigma = estimation.all_covariances(dX, sigmas) # burn-in for jit\n", + "%time Sigma = estimation.all_covariances(dX, sigmas)" ] }, { @@ -956,7 +938,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 61, "metadata": { "scrolled": true }, @@ -975,7 +957,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 62, "metadata": { "scrolled": false }, @@ -1035,23 +1017,23 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 65, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 1.99 ms\n" + "Wall time: 2.99 ms\n" ] }, { "data": { "text/plain": [ - "-48613.762381432694" + "-45684.835800463035" ] }, - "execution_count": 31, + "execution_count": 65, "metadata": {}, "output_type": "execute_result" } @@ -1064,9 +1046,9 @@ "sigma_measurement = np.full(4, 0.05)\n", "\n", "# calculate log-likelihood\n", - "sj.estimation.log_likelihood(Y, Sigma, sigma_measurement)\n", + "estimation.log_likelihood(Y, Sigma, sigma_measurement)\n", "\n", - "%time sj.estimation.log_likelihood(Y, Sigma, sigma_measurement)" + "%time estimation.log_likelihood(Y, Sigma, sigma_measurement)" ] }, { @@ -1081,7 +1063,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 69, "metadata": {}, "outputs": [], "source": [ @@ -1098,10 +1080,10 @@ " M = np.stack([dX1, dX2], axis=2)\n", " \n", " # calculate all covariances\n", - " Sigma = sj.estimation.all_covariances(M, np.array([sigma_persist, sigma_trans]))\n", + " Sigma = estimation.all_covariances(M, np.array([sigma_persist, sigma_trans]))\n", " \n", " # calculate log=likelihood from this\n", - " return sj.estimation.log_likelihood(Y, Sigma, sigma_measurement)" + " return estimation.log_likelihood(Y, Sigma, sigma_measurement)" ] }, { @@ -1113,12 +1095,12 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 70, "metadata": {}, "outputs": [], "source": [ "# stack covariances into matrix using helper function, then do a draw using NumPy routine\n", - "V = sj.estimation.build_full_covariance_matrix(Sigma, sigma_measurement, 100)\n", + "V = estimation.build_full_covariance_matrix(Sigma, sigma_measurement, 100)\n", "Y = np.random.multivariate_normal(np.zeros(400), V).reshape((100, 4))" ] }, @@ -1131,7 +1113,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 71, "metadata": { "scrolled": true }, @@ -1140,7 +1122,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Wall time: 307 ms\n" + "Wall time: 237 ms\n" ] } ], @@ -1151,14 +1133,14 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 72, "metadata": { "scrolled": true }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1213,12 +1195,12 @@ "source": [ "### 7.1 Implementation\n", "\n", - "Our quasi-Newton method can be implemented in two steps. First, build the nonlinear function $H(U, Z).$ Second, guess $U$ for a given $Z$ and iterate until convergence. We automatized both of these, so all we need to do is call a single function, `nonlinear.td_solve`." + "Our quasi-Newton method can be implemented in two steps. First, build the nonlinear function $H(U, Z).$ Second, guess $U$ for a given $Z$ and iterate until convergence. We automatized both of these, so all we need to do is call the `solve_impulse_nonlinear` method for the `ks_model` object. We will also solve for the linearized dynamics using the `solve_impulse_linear` method for comparison." ] }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 77, "metadata": { "scrolled": false }, @@ -1239,10 +1221,10 @@ } ], "source": [ - "Z = ss['Z'] + 0.01*0.8**np.arange(T)\n", + "Z_shock_path = {\"Z\": 0.01*0.8**np.arange(T)}\n", "\n", - "td_nonlin = sj.td_solve(ss=ss, block_list=[firm, household, mkt_clearing],\n", - " unknowns=['K'], targets=['asset_mkt'], H_U=H_K, monotonic=True, Z=Z)" + "td_nonlin = ks_model.solve_impulse_nonlinear(ss, Z_shock_path, unknowns, targets)\n", + "td_lin = ks_model.solve_impulse_linear(ss, Z_shock_path, unknowns, targets)" ] }, { @@ -1256,7 +1238,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 81, "metadata": { "scrolled": true }, @@ -1275,8 +1257,8 @@ } ], "source": [ - "dr_nonlin = 10000 * (td_nonlin['r'] - ss['r'])\n", - "dr_lin = 10000 * G['r'] @ (Z - ss['Z'])\n", + "dr_nonlin = 10000 * td_nonlin.deviations()['r']\n", + "dr_lin = 10000 * td_lin['r']\n", "\n", "plt.plot(dr_nonlin[:50], label='nonlinear', linewidth=2.5)\n", "plt.plot(dr_lin[:50], label='linear', linestyle='--', linewidth=2.5)\n", @@ -1298,7 +1280,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 82, "metadata": {}, "outputs": [ { @@ -1333,13 +1315,14 @@ } ], "source": [ - "Z = ss['Z'] + 0.1*0.8**np.arange(T)\n", + "big_Z_shock_path = {\"Z\": 0.1*0.8**np.arange(T)}\n", "\n", - "td_nonlin = sj.td_solve(ss, [firm, household, mkt_clearing], ['K'], ['asset_mkt'], H_K, monotonic=True, Z=Z)\n", + "td_nonlin = ks_model.solve_impulse_nonlinear(ss, big_Z_shock_path, unknowns, targets)\n", + "td_lin = ks_model.solve_impulse_linear(ss, big_Z_shock_path, unknowns, targets)\n", "\n", "# extract interest rate response, scale to basis points\n", - "dr_nonlin = 10000 * (td_nonlin['r'] - ss['r'])\n", - "dr_lin = 10000 * G['r'] @ (Z - ss['Z'])\n", + "dr_nonlin = 10000 * td_nonlin.deviations()['r']\n", + "dr_lin = 10000 * td_lin['r']\n", "\n", "plt.plot(dr_nonlin[:50], label='nonlinear', linewidth=2.5)\n", "plt.plot(dr_lin[:50], label='linear', linestyle='--', linewidth=2.5)\n", @@ -1349,13 +1332,6 @@ "plt.legend()\n", "plt.show()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/notebooks/rbc.ipynb b/notebooks/rbc.ipynb index d45c357..10912fc 100644 --- a/notebooks/rbc.ipynb +++ b/notebooks/rbc.ipynb @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -61,8 +61,7 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj\n", - "from sequence_jacobian import simple, helper" + "from sequence_jacobian import simple, create_model" ] }, { @@ -118,7 +117,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -156,132 +155,56 @@ "source": [ "## 2 Steady state\n", "\n", - "The next step of solving a model is to compute its steady state. The sequence-jacobian toolkit provides functionality for computing a model's steady state from its DAG representation, but if the user already has a pre-computed steady state they can supply this in the format of a dict(ionary) mapping parameters and variable names to their values." + "The next step of solving a model is to compute its steady state. The sequence-jacobian toolkit provides functionality for computing a model's steady state from its DAG representation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We will use almost exactly the same arrangement of the DAG, with two unknowns and two targets to compute the steady state, providing an initial set of fixed variables/parameters.\n", - "\n", - "The one difference is that we will use $\\varphi$ (`vphi`) as an unknown instead of $L$ because we want to calibrate the steady state aggregate labor supply $L=1$. Verifying that it is a valid unknown, observe that $\\varphi$ influences the $euler$ target indirectly through $L$ mapping into $r$ and the $goods\\_mkt$ target indirectly through $L$ mapping into $C$.\n", + "We will use an alternate arrangement of the DAG, since we want to calibrate a few variable/parameter values in steady state to hit certain targets, such as normalizing $Y = 1$ for a given steady state value of $Z$ or ensuring we solve for the $\\beta$ such that $r = 0.01$.\n", "\n", - "Because the `steady_state` function uses a root-finding algorithm, specified in the keyword argument `solver`, one must either provide a set of initial values as given below with $\\varphi : 0.9$ and $K : 2.$, or a set of bounds (depending on the solver's requirements), provided as a tuple of numerical values e.g. $\\varphi : (0.5, 0.99)$." + "The `solver` keyword argument specifies which root-finding algorithm will be used to solve for the steady state. Any of the generic root-finding algorithms listed in `scipy.optimize` can be used." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 22, "metadata": {}, "outputs": [], "source": [ - "# Solving for the steady state as a standard DAG\n", - "calibration = {\"L\": 1., \"Z\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11, \"beta\": 1/(1 + 0.01)}\n", "blocks = [household, firm, mkt_clearing]\n", - "unknowns_ss = {\"vphi\": 0.9, \"K\": 2.}\n", - "targets_ss = {\"euler\": 0., \"goods_mkt\": 0.}\n", - "ss = sj.steady_state(blocks, calibration, unknowns_ss, targets_ss, solver=\"broyden_custom\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can inspect the values contained in the returned dict(ionary) below to confirm that indeed the targets equations have been satisfied with $goods\\_mkt \\approx 0$ and $euler \\approx 0$ and further we have verified that Walras law is also satisfied by including the resource constraint as an additional variable in the DAG." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'L': 1.0,\n", - " 'Z': 1.0,\n", - " 'r': 0.010000000006606863,\n", - " 'eis': 1.0,\n", - " 'frisch': 1.0,\n", - " 'delta': 0.025,\n", - " 'alpha': 0.11,\n", - " 'beta': 0.9900990099009901,\n", - " 'vphi': 0.965891472871577,\n", - " 'K': 3.6206932399091794,\n", - " 'Y': 1.1520387583703882,\n", - " 'w': 1.0253144949496455,\n", - " 'C': 1.0615214273518796,\n", - " 'I': 0.0905173309977294,\n", - " 'goods_mkt': 2.0779156173489355e-11,\n", - " 'euler': -6.162292898181931e-12,\n", - " 'walras': -2.077937821809428e-11}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ss" + "rbc_model = sj.create_model(blocks, name=\"RBC\")\n", + "\n", + "calibration = {\"L\": 1., \"Z\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11}\n", + "unknowns_ss = {\"vphi\": 0.9, \"beta\": 0.99, \"K\": 2., \"Z\": 1.}\n", + "targets_ss = {\"goods_mkt\": 0., \"r\": 0.01, \"euler\": 0., \"Y\": 1.}\n", + "\n", + "ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "However, suppose we additionally want to normalize $Y = 1$ in steady state. The current structure of the DAG with the unknowns $\\varphi, K$ and targets $euler, goods\\_mkt$ do not deliver this directly; however, we can easily accommodate this by including another unknown and target. In this case, we will choose $Z$ as the unknown and target $Y = 1$. To set a target equal to a non-zero value, if the variable is not written as an implicit function like $euler$ or $goods\\_mkt$, one simply needs to write the desired value in the dictionary in lieu of a 0." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# Solving for the steady state as a standard DAG\n", - "calibration = {\"L\": 1., \"r\": 0.01, \"eis\": 1., \"frisch\": 1., \"delta\": 0.025, \"alpha\": 0.11, \"beta\": 1/(1 + 0.01)}\n", - "blocks = [household, firm, mkt_clearing]\n", - "unknowns_ss = {\"vphi\": 0.9, \"K\": 2., \"Z\": 1.}\n", - "targets_ss = {\"euler\": 0., \"goods_mkt\": 0., \"Y\": 1.}\n", - "ss = sj.steady_state(blocks, calibration, unknowns_ss, targets_ss, solver=\"broyden_custom\")" + "We can inspect the values contained in the returned `SteadyStateDict` object below to confirm that indeed the targets equations have been satisfied with the goods market clearing condition satisfied, the euler equation residual equal to 0, and as an additional check we have verified that Walras law is satisfied by including the resource constraint as an additional variable in the DAG." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 21, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{'L': 1.0,\n", - " 'Z': 0.8816460975729918,\n", - " 'r': 0.010000000009327194,\n", - " 'eis': 1.0,\n", - " 'frisch': 1.0,\n", - " 'delta': 0.025,\n", - " 'alpha': 0.11,\n", - " 'beta': 0.9900990099009901,\n", - " 'vphi': 0.9658914729252107,\n", - " 'K': 3.142857142122498,\n", - " 'Y': 1.0000000000327407,\n", - " 'w': 0.8900000000291391,\n", - " 'C': 0.9214285714043695,\n", - " 'I': 0.07857142855306254,\n", - " 'goods_mkt': 7.530864820637362e-11,\n", - " 'euler': -1.0022205287896213e-11,\n", - " 'walras': -7.530831513946623e-11}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "Goods Market Clearing: 2.220446049250313e-16, Euler equation: 1.1102230246251565e-15, Walras: -4.440892098500626e-16\n" + ] } ], "source": [ - "ss" + "print(f\"Goods Market Clearing: {ss['goods_mkt']}, Euler equation: {ss['euler']}, Walras: {ss['walras']}\")" ] }, { @@ -292,20 +215,20 @@ "\n", "The linearized impulse responses of the model are fully characterized by the general equilibrium Jacobians $G$. These matrices map *any* sequence of shocks into an impulse response, e.g. $dC = G^{C,Z} dZ.$ Once we have them, we're pretty much done!\n", "\n", - "We can get all of these in a single call to the function `jacobian.get_G`. This function takes in the model blocks (in arbitrary order), the names of exogenous shocks, the names of unknown endogenous variables, the names of target equations, the truncation horizon, and the steady state dict." + "We can get all of these in a single call to the `solve_jacobian` method of the `rbc_model` object. This function takes in the `SteadyStateDict` we obtained from calling `rbc_model.solve_steady_state`, the names of exogenous shocks, the names of unknown endogenous variables, the names of target equations, and the truncation horizon." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ - "G = sj.get_G(block_list=blocks,\n", - " exogenous=['Z'],\n", - " unknowns=['K', 'L'],\n", - " targets=['euler', 'goods_mkt'],\n", - " T=300, ss=ss)" + "exogenous = [\"Z\"]\n", + "unknowns = [\"K\", \"L\"]\n", + "targets = [\"euler\", \"goods_mkt\"]\n", + "\n", + "G = rbc_model.solve_jacobian(ss, exogenous, unknowns, targets, T=300)" ] }, { @@ -319,7 +242,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -359,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 49, "metadata": {}, "outputs": [], "source": [ @@ -375,7 +298,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -408,22 +331,59 @@ "For those of you familiar with Dynare, these impulse responses are identical to what you could obtain by running the perfect foresight solver `simul` with the `linear_approximation` option." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that we can also perform the same calculation as the one above using the `solve_impulse_linear` method of the `rbc_model` object." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3dd5wV1fnH8c93O7D0XelVQAVpsqKxl6jYQGMDSywxxkSjRmOi0URjTDSYqLEkSqIxP40itgQr9hZFWBBEQIr0Xpa+bH9+f8wsXpe7ywX27t29+7xfr3ntnTNzZp65e3efO3NmzpGZ4ZxzzlWVkugAnHPO1U+eIJxzzkXlCcI551xUniCcc85F5QnCOedcVJ4gnHPOReUJwtVrkn4l6R+JjsMlhqTukkxSWi1v931Jl9fmNpORJ4gGRtL5kvIlbZW0UtLrko5IdFy1QdIxkpZFlpnZH8zM/5D3gqTbJT21h3VfDz9rWyWVSiqJmH8k/J1VRJRtlfRyxH5Lw7KNkj6R9J3aPToXT54gGhBJ1wP3A38A2gFdgb8CIxIZVzKo7W+oycLMTjazbDPLBv4NjK6cN7Mrw9VWRJRlm9npEZt4NqybC3wMvChJdXwYbg95gmggJLUE7gCuMrMXzWybmZWa2ctmdmO4Tqak+yWtCKf7JWWGy46RtEzSDZLWhGcfl0Zs/xRJsyRtkbRc0s/D8kskfVwlFpPUK3z9hKS/RnzT/J+k9uG+N0j6StLgiLqLJN0c7muDpH9KypLUDHgd6BjxTbRj1W+/koZLmhl+I31f0gFVtv1zSV9I2iTpWUlZ1byfl4Sx3iepALg9LL9M0uwwtgmSuoXlCtddE277C0kHRrwHj0h6K3z/PqisFy4/TNLksN5kSYdFLHtf0u/CWLZIelNSTrgsS9JTktaHxztZUrvKz4Okx8Lf43JJd0pKjXKcw4BfAeeF7+n0sLyjpPGSCiTNl/TD6j57tcHMSoF/Ae2BtlHiHKrgzHizpNWS7q2yygWSlkhaJ+mWiHrVfubD5SMkTQu3+3X4flTdd4fw9xn5mV8Q/j4WSrqgtt6HBsfMfGoAEzAMKAPSaljnDmAisA/BN7ZPgN+Fy44J698BpAOnAIVA63D5SuDI8HVr4KDw9SXAx1X2Y0Cv8PUTwDpgCJAFvAssBL4PpAJ3Au9F1F0EfAl0AdoA/wPujIhxWZV93Q48Fb7uA2wDTgiP4RfAfCAjYtuTgI7htmcDV1bzXl0Svh8/BdKAJsAZ4fYOCMtuBT4J1z8JmAK0AhSu0yHiPdgCHAVkAn+pfM/CODYAF4XbHBXOtw2Xvw98HR5bk3D+7nDZj4CXgabhezkEaBEu+w/wKNAs/H1PAn5UzbHueA8jyj4gOPvMAgYBa4Hjd/EZfKLydxVRttPvrJrfXSZwD7C0mnU/BS4KX2cDh4avuxN83v4evj8DgWLggBg+80OBTeHnJQXoBOwf8b5fHm5/LnBFWN4M2AzsF853APol+u8/UVPCA/Apxl8UXACs2sU6XwOnRMyfBCwKXx8DbCciwQBrIv4Ql4T/kFpU2eYl7DpB/D1i2U+B2RHz/YGNEfOLiPinTZCovo6IsaYE8WtgXMSyFGA5cEzEti+MWD4aeKSa9+oSYEmVsteBH1TZfiHQDTgu/EdyKJBSpd4TwNiI+WygnCAJXgRMqrL+p8Al4ev3gVsjlv0EeCN8fVn4D29AlfrtCP5JNokoG0VEIq7uPQznu4TxNY8ouwt4YhefryeIniAqgI0R07kR+y0Jy9YQfHkYUs22PwR+C+RUKe8eft46R5RNAkbG8Jl/FLivmv29D9wbfmZGRZQ3C+M9K/L9bayTX2JqONYDOar5WnlHYHHE/OKwbMc2zKwsYr6Q4J8ZBH8QpwCLw0sku9OYuDri9fYo89nfXp2lNcRYk28dn5lVhNvqFLHOqojXkccXzdIq892Av4SXczYCBQRnC53M7F3gIeBhYLWkMZJaRNuWmW0N63asGnNocYwxPwlMAMaGl09GS0oP40wHVkbE+ijBt+hYdAQKzGxLDTHtjhVm1ipiGhexbFxYto+ZHWdmU6rZxg8IzqK+Ci+lnVZleXXvUU2f+S4ECaQ6FxB8wXi+ssDMtgHnAVcSvL+vStq/hm0kNU8QDcenQBHBZZDqrCD451Gpa1i2S2Y22cxGEPyT+Q9Q+Ue+jeASBwCS2u9GzNXpUk2Mu+pa+FvHJ0nhtpbvYRxV97eU4DJN5D+7Jmb2CYCZPWBmQ4B+BP/Mboyou+OYJGUTXFpaUTXmUNdYYragjem3ZtYXOAw4jeDS3VKCM4iciDhbmFm/GI9zBdBGUvPdjSlezGyemY0i+Pz9EXg+bJfalZo+80uBfWuoezvB5dGnI9tvzGyCmZ1AcHnpK4LLW42SJ4gGwsw2Ab8BHpZ0hqSmktIlnSxpdLjaM8CtknLDhs7fALu8vVFShqQLJLW0oDFxM8ElCIDpQD9Jg8IG39tr4XCuktRZUhuCBtRnw/LVQFsFDfLRjANOlXR8+E36BoJ/lJ/UQkwAjwA3S+oHOxqCzwlfHyzpkHC/2wiSdXlE3VMkHSEpA/gd8JmZLQVeA/oouD05TdJ5QF/glV0FI+lYSf3Df16bgVKg3MxWAm8Cf5bUQlKKpH0lHV3NplYD3SWlAIRxfQLcFTaEDyD4Bv/v3XmzapOkCyXlhmeFG8Pi8prqhGr6zD8GXBp+XlIkdapyNlAKnENwWenJcJ12Cm6EaEbw2doaYxxJyRNEA2Jm9wLXEzSeriX4hnQ1wTd+CBqE84EvgBnA1LAsFhcBiyRtJji9vjDc51yChsC3gXkEtyruracJ/sEtCKc7w319RfAHvyC8dPKtS09mNieM60GCb36nA6ebWUktxISZvUTw7XVs+D58CZwcLm5B8E1yA8FljPXAn6oc020El5aGEFy+wMzWE3zzvyGs8wvgNDNbF0NI7Qkuf2wmaHD/gG/++X0fyABmhTE9T/CNN5rnwp/rJU0NX48iuL6/AngJuM3M3oohpngZBsyUtJWgkX+kmRXFUK/az7yZTQIuBe4jaKz+gCpnc+Fn53sEZy6PE9xIcAPB+1IAHE3QLtQoKWyYca5OSFoEXG5mbyc6ltoi6QmCxvVbEx2Lc7XJzyCcc85F5QnCOedcVH6JyTnnXFR+BuGccy6qpOmgLCcnx7p3757oMJxzrkGZMmXKOjPLjbYsaRJE9+7dyc/PT3QYzjnXoEiq+qT/Dn6JyTnnXFSeIJxzzkXlCcI551xUniCcc85F5QnCOedcVJ4gnHPOReUJwjnnXFRJ8xxEXVlaUMgNz03ni2UbyW2eSfsWWbRrkUX7Flm0b5nFvvtkc1TvXFJTlOhQnXNur3iC2A1fLt/EpU9MZu2WYgCWFmxnacH2ndY7olcOD44aTOtmGXUdonPO1Rq/xBSj9+as4dxHP92RHE7p357hAzsytEcburVtSkbaN2/lx/PXMfzhj5m1YnOiwnXOub0W1zMIScMIRodKBf5hZndXs97ZBKNeHWxm+WHZzQTDIJYD15jZhHjGWpNnJi3h1v98SXmFIcHtp/fj4sO6f2sdM2Pd1hJuHz+TV2esZGnBdr73t/9xz9kDOX1gx+gbds65eixuZxDhOLoPEwzZ2BcYJalvlPWaA9cAn0WU9QVGEgwOPwz4a+Sg4nXFzPjzm3O4+cUZlFcYmWkp/O2CITslBwBJ5DbP5KHzB/PLYfsjQVFpBT995nPuen025RXerbpzrmGJ5yWmocB8M1sQjvs6FhgRZb3fAaMJBoGvNAIYa2bFZrYQmB9ur86UVxg3PDedB9+dD0Drpuk8/cNDGXZg+xrrSeLHx+zLE5cOpUVWcIL26AcLuOSfk9hYWCtDJzvnXJ2IZ4LoBCyNmF8Wlu0gaTDQxcxe2d26Yf0rJOVLyl+7dm3tRB16dvJSXpy6HIBubZvy4k8OZ0i31jHXP7pPLuOvPoL92jUH4KN567jkn5MpLiuv1Tidcy5e4pkgot3nueM6i6QU4D7ght2tu6PAbIyZ5ZlZXm5u1O7M90hFhfGPjxcA0LFlFi/8+DB65DTb7e10z2nGiz85jJP6tQNg2tKN/O6VWbUWp3POxVM8E8QyoEvEfGdgRcR8c+BA4H1Ji4BDgfGS8mKoG1fvzVnDgrXbALjsiB7kZGfu8baaZabxwKjBDOrSCoCnJi7h+SnLaiVO55yLp3gmiMlAb0k9JGUQNDqPr1xoZpvMLMfMuptZd2AiMDy8i2k8MFJSpqQeQG9gUhxj/ZZ/fLQQgOzMNM49uMsu1t61zLRU/nbhQbQNn4u45aUZfLl8015v1znn4iluCcLMyoCrgQnAbGCcmc2UdIek4buoOxMYB8wC3gCuMrM6uXj/5fJNfLpgPQAjD+5Ci6z0Wtluh5ZNeHDUYFIExWUV/PjfU7zR2jlXr8X1QTkze83M+pjZvmb2+7DsN2Y2Psq6x1Q+AxHO/z6st5+ZvR7POCM99nFw9pCaIi45vHutbvuwXjn8ctj+QPAU9rVjp1Hht7865+opf5I6wqpNRbw8PWjqOPnA9nRu3bTW93HFUT0Z1i+4VfaDuWv5yzvzan0fzjlXGzxBRHjik0WUhd/oLz+yZ1z2IYl7zhnAvrnBXVF/eWce7361Oi77cs65veEJIrStuIynP1sMQF631jvuOoqH5lnpPHrREJplBA+HXz9uOuu3Fsdtf845tyc8QYSen7KMzUVlQPzOHiL12qc5d501AICNhaXc+ersuO/TOed2hycIgm41Khunu7Zpygl929XJfk8f0IHvHhDs66XPl/PRvNp9Gtw55/aGJwjgrVmrWVJQCMBlh3evs8F+JHHHiH47LjXd8tKXbC/xrjicc/WDJwjgsbBbjRZZaZyTt/cPxu2Ojq2a8POT9gNgSUEhD7zrdzU55+qHRp8gpi3dyORFGwA4/5BuNMus+0H2vv+d7gzs3BKAMR8uYPZKH2jIOZd4jT5B/OOj4OwhLUVcfFi3hMSQmiLu+t4AUlNEeYVxUzj+hHPOJVKjTxCDu7amQ8ssTh/YkQ4tmyQsjr4dW3D5kT0AmL50I09NXJywWJxzDkBmyfFNNS8vz/Lz83e9YhSl5RVsLSqjddiZXqJsLynnxPs/YGnBdpplpPL2DUcnNGk555KfpClmlhdtWaM/gwBIT01JeHIAaJKRyp1n9AdgW0k5t/13ZoIjcs41Zp4g6pmj++RyxqCOALw5azXvzVmT4Iicc42VJ4h66NbT+tI8vJvq96/OprS8IsEROecaI08Q9VBOdiZXHdcLgPlrtvLMpCUJjsg51xjFNUFIGiZpjqT5km6KsvxKSTMkTZP0saS+YXl3SdvD8mmSHolnnPXRpYd3p0uboIH6vrfmsqmwNMEROecam7glCEmpwMPAyUBfYFRlAojwtJn1N7NBwGjg3ohlX5vZoHC6Ml5x1leZaancfPIBAGwoLOWh9/wJa+dc3YrnGcRQYL6ZLTCzEmAsMCJyBTOLfGS4GZAc99zWkpMPbM/Q7m2AYKyKReu2JTgi51xjEs8E0QlYGjG/LCz7FklXSfqa4AzimohFPSR9LukDSUdG24GkKyTlS8pfuzb5ekKVxK2nBWcRpeXGXa97l+DOuboTzwQRrUvUnc4QzOxhM9sX+CVwa1i8EuhqZoOB64GnJbWIUneMmeWZWV5ubm4thl5/DOjciu8dFOTVCTNX8+nX6xMckXOusYhnglgGRHaN2hlYUcP6Y4EzAMys2MzWh6+nAF8DfeIUZ733i5P2Jys9+FXd+eos76fJOVcn4pkgJgO9JfWQlAGMBMZHriCpd8TsqcC8sDw3bORGUk+gN7AgjrHWa+1bZvGjo/YFYOaKzbwwdVmCI3LONQZxSxBmVgZcDUwAZgPjzGympDskDQ9Xu1rSTEnTCC4lXRyWHwV8IWk68DxwpZkVxCvWhuBHR/ekXYtMAO6ZMIdtxWUJjsg5l+y8s74G5IUpy7jhuekAXHt8b352QqO96uacqyXeWV+SOHNwJw7sFLTV/+OjBazdUpzgiJxzycwTRAOSkiJ+OWx/IOjt9SEfntQ5F0eeIBqYI3vnckSvHACenrSExev94TnnXHx4gmiAKs8iSsuNP785N8HROOeSlSeIBqh/55acPjAYM2L89BV8uXxTgiNyziUjTxAN1M9P7ENaSvCw+h/f+CrB0TjnkpEniAaqW9tmXHBIVwA+mreOj+YlX19UzrnE8gTRgP30+N40y0gFgrOICu+CwzlXizxBNGA52Zn88KieAHy5fDOvzFiZ4Iicc8nEE0QDd/mRPcnJzgDgTxPmUFLm41c752qHJ4gGLjszjWuOD/o8XFJQ6ONXO+dqjSeIJDDy4K50a9sUgAffnecd+TnnaoUniCSQkZbC9WHHfeu2lvDYxwsTHJFzLhl4gkgSpw/oSN8OQUd+Yz5cQMG2kgRH5Jxr6DxBJImUFPGLYfsBsLW4jIffm5/giJxzDV1cE4SkYZLmSJov6aYoy6+UNEPSNEkfS+obsezmsN4cSSfFM85kcXSfXA7p0QaAJz9dzPKN2xMckXOuIdtlgpB0uKRm4esLJd0rqVsM9VKBh4GTgb7AqMgEEHrazPqb2SBgNHBvWLcvwRCl/YBhwF8rhyB11ZPEL08OOvIrKa/g/re8Iz/n3J6L5Qzib0ChpIHAL4DFwP/FUG8oMN/MFphZCTAWGBG5gpltjphtBlQ+CjwCGGtmxWa2EJgfbs/twkFdW3Ni33YAvDB1GfNWb0lwRM65hiqWBFFmwbikI4C/mNlfgOYx1OsELI2YXxaWfYukqyR9TXAGcc3u1HXR3XjSfqQIKiwYv9o55/ZELAlii6SbgQuBV8NLPekx1FOUsp06CzKzh81sX+CXwK27U1fSFZLyJeWvXeud1VXq3a45Zx3UGYA3Z61m6pINCY7IOdcQxZIgzgOKgR+Y2SqCb/L3xFBvGdAlYr4zsKKG9ccCZ+xOXTMbY2Z5ZpaXm5sbQ0iNx3Un9CEjNfj1/vH1rwhOAp1zLnYxnUEQXFr6SFIfYBDwTAz1JgO9JfWQlEHQ6Dw+cgVJvSNmTwUqB1keD4yUlCmpB9AbmBTDPl2oU6smXPSd4F6CzxYW8MFcP8Nyzu2eWBLEh0CmpE7AO8ClwBO7qmRmZcDVwARgNjDOzGZKukPS8HC1qyXNlDQNuB64OKw7ExgHzALeAK4ys/LdOjLHVcf2IjszDYA/vjHHuwN3zu0W7erSg6SpZnaQpJ8CTcxstKRp4a2p9UZeXp7l5+cnOox654F35nFveLvrfecN5MzBnRMckXOuPpE0xczyoi2L5QxCkr4DXAC8Gpb5MwkNxOVH9iAnOxOAP02YS3GZn4g552ITS4K4FrgZeCm8RNQTeC++Ybna0jQjjeu+GzT1LN+4nacmenfgzrnY7DJBmNmHZjbczP4Yzi8ws2t2Vc/VH+cd3IWeOc0AeOjdeWwuKk1wRM65hsA762sE0lNTuPGkoCO/DYWlPPrB1wmOyDnXEHiCaCSGHdieQV1aAfDYxwtZtakowRE55+o7TxCNhCRuDjvyKyqt4C/veEd+zrmaxdKba4+wB9cXJY2vnOoiOFe7DunZluP33weAZycvZf4a78jPOVe9WM4g/gMsAh4E/hwxuQboF8P2R2FHfqPf8I78nHPVS4thnSIzeyDukbg6sV/7oCO/56cs481Zq8lfVEBe9zaJDss5Vw/FcgbxF0m3SfqOpIMqp7hH5uLm+hP6kJEW/Orv8o78nHPViCVB9Ad+CNzNN5eX/hTPoFx8dWzVhEsP6w7AlMUbmDBzVWIDcs7VS7EkiDOBnmZ2tJkdG07HxTswF18/ObYXrZsGw3rc9fpXlJRVJDgi51x9E0uCmA60incgrm61bJLOtccHXXAsXl/I/326KKHxOOfqn1gSRDvgK0kT/DbX5HLBod12dMHx4Lvz2VhYkuCInHP1SSx3Md0W9yhcQqSnpnDTyftzxZNT2LS9lAfemc9vTu+b6LCcc/VELJ31fQB8BTQPp9lhmUsCJ/RtxyE9gttcn5y4iEXrtiU4IudcfRHLk9TnEgz3eQ5wLvCZpLNj2bikYZLmSJov6aYoy6+XNEvSF5LekdQtYlm5pGnh5Je04kQSvz6tLxKUlht3v/5VokNyztUTsbRB3AIcbGYXm9n3gaHAr3dVSVIq8DBwMtAXGCWp6vWLz4E8MxsAPA+Mjli23cwGhdNwXNwc2KklZw7uBMAbM1fx2YL1CY7IOVcfxJIgUsxsTcT8+hjrDQXmh+NHlABjgRGRK5jZe2ZWGM5OBHw8zAS58aT9yEoPfq2/f222j1/tnIvpH/0b4R1Ml0i6hGDY0ddiqNcJWBoxvywsq84PgNcj5rMk5UuaKOmMaBUkXRGuk7927doYQnLV6dCyCT88sicAXyzbxPjpKxIckXMu0WJppL4ReBQYAAwExpjZL2PYtqJtLuqK0oVAHnBPRHHXcCDt84H7Je0bJbYxZpZnZnm5ubkxhORq8qOj990xfvXoN76iqNTHr3auMasxQUhKlfS2mb1oZteb2c/M7KUYt70M6BIx3xnY6WuppO8StHMMN7PiynIzWxH+XAC8DwyOcb9uD2VnpnHDiX0AWLGpiEc/WJDgiJxziVRjgjCzcqBQUss92PZkoHc4nkQGMBL41t1IkgYTnJ0Mj2znkNRaUmb4Ogc4HJi1BzG43XRuXhf6dmgBwF/fn8+yDYW7qOGcS1axtEEUATMkPSbpgcppV5XMrAy4GpgAzAbGmdlMSXdIqrwr6R4gG3iuyu2sBwD5kqYD7wF3m5kniDqQmiJ+O6IfAMVlFfzhtdkJjsg5lyjaVVfPki6OVm5m/4pLRHsoLy/P8vPzEx1G0rh27Of8d1pwRfDpyw/hsF45CY7IORcPkqaE7b07qfYMQtI74cu+ZvavqlNcInX1xs0nH0DTjFQAbn95JmXl3turc41NTZeYOkg6GhguaXDkYEE+YFDya98yi6uO7QXA3NVbeXLi4gRH5JyrazV11vcb4CaCu4/urbLMAB8TIsldfmQPxuUvZfH6Qu59ay7DB3akbXgbrHMu+VV7BmFmz5vZycDoiIGCfMCgRiQzLZXfnBb0jrKlqIx7JsxJcETOuboUy4Nyv6uLQFz9dNz++3DMfsFDiM/mL+WLZRsTHJFzrq7Ecpura8Qk8ZvT+pKeKszg9vEzvZ8m5xoJTxBul3rmZnPZET0AmLpkIy9MXZbgiJxzdSGmBBF2udFRUtfKKd6Bufrlp8f1Zp/mQQP1H16bTcE2H57UuWQXy4BBPwVWA28R9OT6KvBKnONy9Ux2Zhq3nR48Yb2hsJS7/Alr55JeLGcQ1wL7mVk/M+sfTgPiHZirf07p355jwwbr56YsY6IPLORcUoslQSwFNsU7EFf/SeKOEQfuGFjolpdmUFzmXYI7l6xiSRALgPcl3RyOIX29pOvjHZirn7q0acp13w26BP967TbvEty5JBZLglhC0P6QATSPmFwj9YMjerB/++Aj8NB781m4bluCI3LOxcMue3PdsaLUHDAz2xrfkPaM9+Zat6Ys3sDZj3yCGRzeqy1P/eAQpGiDCDrn6rM96s01ovKBkj4HvgRmSpoiqV9tB+kaliHdWnP+0OBu5//NX89/pi1PcETOudoWyyWmMcD1ZtbNzLoBNwB/j2XjkoZJmiNpvqSboiy/XtIsSV9IekdSt4hlF0uaF05Rx6RwifWLYfvvGMP6zldms7HQn41wLpnEkiCamdl7lTNm9j7QbFeVJKUCDwMnA32BUZL6VlntcyAvvG32eWB0WLcNcBtwCDAUuE1S6xhidXWoZZN0fnN68Ctdv62E37/qz0Y4l0xiuotJ0q8ldQ+nW4GFMdQbCsw3swVmVgKMBUZErmBm75lZ5aDHEwm6Fgc4CXjLzArMbANBI/mwWA7I1a3TB3Tg6D7fPBvx3pw1u6jhnGsoYkkQlwG5wIvAS+HrS2Oo14ngGYpKy8Ky6vwAeH136kq6QlK+pPy1a9fGEJKrbZL4w/f6k50ZDC1y8wsz2LS9NMFROedqQyzdfW8ws2vM7CAzG2xm14bf6ncl2i0tUW+ZknQhkAfcszt1zWyMmeWZWV5ubm4MIbl46NSqCbecegAAqzYXcecrsxIckXOuNtQ0JvX94c+XJY2vOsWw7WVAl4j5zsCKKPv5LnALMNzMinenrqs/Rh7chSN75wDhpaav/FKTcw1dtc9BSBpiZlPCcal3YmYf1LhhKQ2YCxwPLAcmA+eb2cyIdQYTNE4PM7N5EeVtgClA5djXU4EhZlZQ3f78OYjEW75xOyfd9yFbi8to3yKLCT87ipZN0hMdlnOuBnv0HISZTQlfDjKzDyInYNCudmpmZcDVwARgNjDOzGZKukPS8HC1e4Bs4DlJ0yrPTMJE8DuCpDIZuKOm5ODqh06tmnCrX2pyLmns8klqSVPN7KAqZZ+b2eC4Rrab/AyifjAzvv/4JD6atw6Af15yMMfuv0+Co3LOVWePziAkjZL0MtCjSvvDe4D38+yiksTdZw3YcVfTTS9+4Xc1OddA1XQX0yfAn4Gvwp+V0w34MwmuBpGXmlZvLuaOl/1Sk3MNUVp1C8xsMbAY+E7dheOSxXkHd+G1L1fx4dy1vDB1Gcfsl8vpAzsmOizn3G6IpbO+QyVNlrRVUomkckmb6yI413BJYvRZA2jVNLiL6VcvzWDZhsJd1HLO1SexPEn9EDAKmAc0AS4HHoxnUC45tG+ZxeizgtFptxSVcd3YaZSVVyQ4KudcrGJJEJjZfCDVzMrN7J/AsfENyyWLE/u158JDg27B8xdv4KH35ic4IudcrGJJEIWSMoBpkkZL+hkx9ObqXKVbTulL732yAXjgnXnkL/JHWpxrCGJJEBcBqQQPvW0j6ALjrHgG5ZJLk4xUHhg1mIy0FCoMrh07zW99da4BiKWzvsVmtt3MNpvZb83s+vCSk3MxO6BDC3518v5A0CXHLS/NINbhbp1ziVHTg3Ljwp8zwhHfvjXVXYguWVx8WHeO3S/odfeVL1by/JRlCY7IOVeTap+DAK4Nf55WF4G45CeJe84ZyLD7P2Ld1mJuGz+TgV1a0add82y4fwkAABkiSURBVESH5pyLoqbO+laGL78HlIWXmnZMdROeSzY52Zncd95AJCgsKefKJ6ewpcjbI5yrj2JppG4BvCnpI0lXSWoX76Bccjuydy7XHd8HgAXrtvHz56Z7e4Rz9VAsjdS/NbN+wFVAR+ADSW/HPTKX1H56XK8d7RETZq7m0Q8XJDgi51xVMT0oF1oDrCLoydX7b3Z7JSVF3H/eYLq2aQrA6De+4pP56xIclXMuUix9Mf1Y0vvAO0AO8EMzGxDLxiUNkzRH0nxJN0VZfpSkqZLKJJ1dZVl5OIjQjoGEXHJp2TSdv114EJnh8xE/feZzVmzcnuiwnHOhWM4gugHXmVk/M7vNzGLqu1lSKvAwcDLQFxglqW+V1ZYAlwBPR9nEdjMbFE7Doyx3SaBfx5b84cz+AKzfVsJP/j2V4rLyBEflnIPY2iBuArIlXQogKVdSjxi2PRSYb2YLzKwEGAuMqLLtRWb2BeA9uDViZw3pvKO/pmlLN/I7H6rUuXohlktMtwG/BG4Oi9KBp2LYdidgacT8srAsVlmS8iVNlHTGbtRzDdCvT+vLoC6tAHhq4hKemuh3UjuXaLFcYjoTGE7QDxNmtgKI5ckmRSnbnXsZu4bjpJ4P3C9p3512IF0RJpH8tWvX7samXX2TmZbK3y48iJzsDABuGz+TD+f679S5RIolQZRYcJO6AUiKtSfXZQQd+1XqDKyINbAwEWFmC4D3gcFR1hljZnlmlpebmxvrpl091aFlE8Z8P4/MtBTKK4yr/j2Vuau3JDos5xqtWBLEOEmPAq0k/RB4G/h7DPUmA70l9Qi7Cx8JxHQ3kqTWkjLD1znA4YBfmG4EDuramj+fOxCALcVlXPrPyazdUpzgqJxrnGJppP4T8DzwArAf8Bsz2+WIcmZWRtBF+ARgNjDOzGZKukPScABJB0taBpwDPCppZlj9ACBf0nTgPeDuWO+ecg3faQM6cuNJ+wFBz69XPJlPUanf2eRcXVOydHGQl5dn+fn5iQ7D1RIz48bnv9jR4+upAzrw4MjBpKREa9pyzu0pSVPC9t6d1NTd9xZJm6ub4heuc0HPr384sz+H9GgDwKtfrOTet+YmOCrnGpeaenNtbmYtgPuBmwhuUe1McMvrnXUTnmvMMtJSePSiIfTMCe6LeOi9+YydtCTBUTnXeMTSSH2Smf3VzLaEo8r9DR9y1NWRVk0zePySg2ndNB2Am1+awatfrNxFLedcbYglQZRLukBSqqQUSRcA3mLo6kz3nGY8fsnBNM1IxQyue/Zz3p+zJtFhOZf0YkkQ5wPnAqvD6ZywzLk6M7hra/7+/TwyUlMoLTeufGoK+YsKEh2Wc0ktlttcF5nZCDPLMbNcMzvDzBbVQWzOfcvhvXJ4YNRgUgRFpRVc+sRkZq7YlOiwnEtauzMehHMJN+zA9ow+O3yQrqiMix+fxIK1WxMclXPJyROEa3DOHtKZ35wW9By/bmsJFz02yceRcC4OPEG4BumyI3pw7fG9geBp61F/n8hyTxLO1aqYE4SkQyW9K+l/3v22qw+u+25vLjs8GJpk8fpCzn3kU5asL0xwVM4lj5qepG5fpeh6gm6/hwG/i2dQzsVCEr8+7QAuPyJIEss3bue8MZ+ycN22BEfmXHKo6QziEUm/lpQVzm8kuL31PMC72nD1giRuOfUAfnxMMFzIyk1FnPfop8xf492EO7e3aupq4wxgGvCKpIuA6wiGBm0K+CUmV29I4hcn7bejTWLNlmJGjpnInFWeJJzbGzW2QZjZy8BJQCvgRWCOmT1gZj7Ul6tXJPGzE/rs6CZ83dYSRo75lC+X+3MSzu2pmtoghkv6GHgX+JJgwJ8zJT0TbfhP5+qDq47txa9O2R+ADYWljBozkU/mr0twVM41TDWdQdxJcPZwFvBHM9toZtcDvwF+XxfBObcnrjhqX24/PXhOYktxGRf/cxL/+Xx5gqNyruGpKUFsIjhrGAns6BnNzOaZ2chYNi5pmKQ5kuZLuinK8qMkTZVUJunsKssuljQvnC6O7XCcC1xyeA8eGDV4R99N1z07jb++P59kGSDLubpQU4I4k6BBuow96JxPUirwMHAy0BcYJalvldWWAJcAT1ep2wa4DTgEGArcJqn17sbgGrfhAzvyr8uG0jwrDYDRb8zh1//9kvIKTxLOxaKmu5jWmdmDZvaIme3Jba1DgflmtsDMSoCxwIgq+1hkZl8Q3B0V6STgLTMrMLMNwFsEz184t1u+s29bXvjxYXRoGdyt/dTEJVz51BS2l3iP9c7tSjy72ugELI2YXxaW1VpdSVdIypeUv3at31jlouvTrjkv/eRw9m/fHIC3Zq1m5N8nsnpzUYIjc65+i2eCiDa6fKzn9jHVNbMxZpZnZnm5ubm7FZxrXNq3zGLcld/h8F5tAZi+dCOnPfgxUxb7mBLOVSeeCWIZ0CVivjOwog7qOhdVi6x0/nnJUEYeHHy01oYP1P37s8XeeO1cFPFMEJOB3pJ6SMoguBtqfIx1JwAnSmodNk6fGJY5t1cy0lK4+6wB/OHM/qSnitJy45aXvuTmF2dQXObtEs5FiluCMLMy4GqCf+yzgXFmNlPSHZKGA0g6WNIygmFMH5U0M6xbQNAh4ORwuiMsc65WnH9IV8ZecSi5zTMBGDt5Kec9OpFVm7xdwrlKSpZT67y8PMvPz090GK6BWb25iB8/NYWpSzYCkJOdyX3nDeTI3t6m5RoHSVPMLC/aMh8wyDVq7Vpk8cwVh3L+IV0BWLe1mIsem8Rdr82mpKzq3dfONS6eIFyjl5mWyh/O7M/oswfQJD0VgEc/XMBZf/vEx5ZwjZonCOdC5+Z14ZVrjuDATi0AmLF8E6c+8BHP5S/1u5xco+QJwrkI++Zm88KPD+OHRwaj1BWWlHPj819wzdhpbCosTXB0ztUtTxDOVZGZlsotp/blX5cNJSc7uMvp5ekr+O59H/DGl6sSHJ1zdccThHPVOLpPLm9cdyTH7hfc0bR2SzFXPjWFn/x7Cmu2+O2wLvl5gnCuBjnZmTx+ycH8+ZyBtGySDsBrM1Zxwr0f8vyUZd424ZKaJwjndkESZw3pzNvXH80p/dsDsGl7KT9/bjrff3wSSwsKExyhc/HhCcK5GOU2z+SvFwzhkQuH7HgC+6N56zj+3g+49805FJaUJThC52qXJwjndtOwA9vz9s+O5ty8zgCUlFXwwLvzOf7PH/Dy9BV+2cklDU8Qzu2Blk3TGX32QF748Xfo36klACs3FfHTZz7nvDETmbViT8bYcq5+8QTh3F4Y0q0N/7nqcO7+Xn/aNssAYNLCAk578CNufvELVm7anuAIndtz3lmfc7Vk0/ZSHnhnHv/6ZBFl4bjXGWkpfP/Qbvz4mH1pGz5T4Vx9UlNnfZ4gnKtl89ds4a7XvuKdr9bsKGuWkcoPjuzJ5Uf2oEVWegKjc+7bPEE4lwBTFhcw+o05fLbwm6FMWjVN50dH7cuFh3aluScKVw8krLtvScMkzZE0X9JNUZZnSno2XP6ZpO5heXdJ2yVNC6dH4hmnc/EwpFsbxl5xKP932dAdDdkbC0v54xtfcfjd7/KnCXNYt7U4wVE6V724nUFISgXmAicQjDE9GRhlZrMi1vkJMMDMrpQ0EjjTzM4LE8UrZnZgrPvzMwhXn5kZE2au4s9vzmXemq07yjPTUjjv4C788MiedGnTNIERusYqUWcQQ4H5ZrbAzEqAscCIKuuMAP4Vvn4eOF6S4hiTcwkhiWEHdmDCdUcx5qIhDOrSCoDisgr+79PFHPOn97l27OdMW7oxwZE69420OG67E7A0Yn4ZcEh165hZmaRNQNtwWQ9JnwObgVvN7KOqO5B0BXAFQNeuXWs3eufiICVFnNivPSf0bcdnCwv42/tf88HctZRXGP+dtoL/TlvBwM4tueg73TltQAeywgGMnEuEeCaIaGcCVa9nVbfOSqCrma2XNAT4j6R+Zvatp4/MbAwwBoJLTLUQs3N1QhKH9mzLoT3b8uXyTTz64QJem7GS8gpj+rJNTH9uOr9/dRbnHdyVCw/tSufWfvnJ1b14XmJaBnSJmO8MrKhuHUlpQEugwMyKzWw9gJlNAb4G+sQxVucS5sBOLXlw1GD+98vjuOb43jv6edpQWMojH3zNUaPf4/uPT2L89BUUlZYnOFrXmMSzkTqNoJH6eGA5QSP1+WY2M2Kdq4D+EY3U3zOzcyXlEiSKckk9gY/C9Qp23lPAG6ldsigpq+CNmat48tNFTF604VvLWmSlcfrAjpw9pDODurTCm+zc3krYcxCSTgHuB1KBx83s95LuAPLNbLykLOBJYDBQAIw0swWSzgLuAMqAcuA2M3u5pn15gnDJaOaKTTw7eSn/nbaCTdu/PeRpr32yOWNQR07p34GeudkJitA1dP6gnHMNXHFZOe/MXsNz+Uv5YO5aKqr82fbt0IJTB3Tg1P4d6J7TLDFBugbJE4RzSWTN5iJe+nw5L32+nK9Wbdlpeb+OLTj5wPYcf0A79m/f3C9DuRp5gnAuSc1fs4VXv1jFqzNWMHf11p2Wd2rVhOP234fjD9iHQ3u29dtm3U48QTjXCMxbvYVXZ6zktRkroyaLphmpHN4rhyN753B4rxx65jTzswvnCcK5xmbJ+kLenr2ad79aw2cL11NavvPfeceWWRzeK4cjeudw2L45O26vdY2LJwjnGrHNRaV8NHcd78xezUfz17F2S/QOAnvmNmNo9zYc3L0NQ3u0oXPrJn6G0Qh4gnDOAUGngXNXb+WjeWv53/x1fLawgMKS6A/ftW+RxcE92jC4SysGdW1F3w4tvA0jCXmCcM5FVVJWwdQlG5i4YD2TFxUwdfFGtlfztHZ6qjigQwsGdm7FoC6t6N+5JT1zmpGW6iMXN2SeIJxzMSktr2Dmis1MXljApEUFTFm8gYJtJdWun5GWwv7tm9OvYwv6dmhB344t2K99C7Iz49nNm6tNniCcc3vEzFi2YTvTlm5k+tKNTF+2kRnLN1FUWlFjvU6tmtCnXTZ92jWnd7vm9GmXTa99smma4YmjvvEE4ZyrNaXlFcxbvZWZKzYxa+VmZq7YzOwVm9lSXLbLuh1aZtEjp9mOad/cbLrnNKNTqyZkpPmlqkSoKUF4OnfO7Zb01BT6dgwuJ1UyM5YWbGfWyk3MWbWVuWu2MG/1Fhas3UZZRL8gKzcVsXJTEZ98vf5b20wRdGjZhK5tmgZT26Z0adOUTq2a0Ll1E3KzM0lJ8Tuq6pqfQTjn4qakrIJF67cxd/UWvl6zjYXrtrJw3TYWrN0W0xlHpfRU0b5lFp1aNaFjqyZ0aJlF+xZZtGuRRfvwddvsTFI9iew2P4NwziVERloKfdo1p0+75t8qNzPWbS1h4bogaSwt2M6SgkKWFBSytKCQ9VUaxkvLgzOUpQXbq91XaorIzc4kt3k4RbzOyc6kTbMM2mZn0LZZBq2aZngyiYEnCOdcnZO045/30B5tdlq+tbiMpQWFrNi4nRUbt7Ns43ZWbCxixcbtLN+wnTVbinbq0ba8wli1uYhVm4ti2D+0bppBm2YZtG6aTqumwc/WTYPk0appOq2apNOiSTotw6lFk3SaZ6Y1qktdniCcc/VOdmYaB3RowQEdWkRdXl5hrNtazKpNQUJYvbmIVZuKWLulmLVbi4OfW4pZt7V4p0QCYAYF20pqvIU3GimIrUVWOs2z0sjOTKN5VhrNs9LJDuezM9NolplGdmYqzcLXzTLSaJoRzDfNSA2ntHp/FhPXBCFpGPAXggGD/mFmd1dZngn8HzAEWA+cZ2aLwmU3Az8gGDDoGjObEM9YnXMNR2qKaBe2QQysYb3yCmNDYQnrthZTsLWEddtKKNhazPptJazfVkLB1hI2FJawaXspGwpL2FBYSklZ9bfwmsGWojK2FMXeflKTjLQUmmak0iQ9lSbhz6YZqWSlp5KZlkpWegpZ6UF55evMtBQy01LJTE8hM+2bsoO6tmafFlm1EleluCUISanAw8AJBGNPT5Y03sxmRaz2A2CDmfUKhxz9I3CepL7ASKAf0BF4W1IfM/MBeZ1zMUtNETnZQRtELMyM7aXlbCgsZfP2UjZFTJXzW4rK2FwU/NxaVMaW4m9eby0uo7iGBFNVSVkFJWUVbKR01yvvwuOX5HFcQ0kQwFBgvpktAJA0FhgBRCaIEcDt4evngYcU9A42AhhrZsXAQknzw+19Gsd4nXONnCSaZqTRNCONTq2a7NE2SssrKCwuZ2vJN0lje0k520q++VlYHM6XllNUUk5hSTnbS8vZHr4uKiunqLSC4tJyikrLKSqrYHtJOcVl5VEvmQFkptV+P1nxTBCdgKUR88uAQ6pbx8zKJG0C2oblE6vU7VR1B5KuAK4A6Nq1a60F7pxzeyo9NYWWTVNo2TS91rdtZpRVGMVlFRSVllNcFiSR4rIKurRpWuv7i2eCiNb6UjX3VbdOLHUxszHAGAieg9jdAJ1zriGRRHqqSE9NqZP+ruL5bPsyoEvEfGdgRXXrSEoDWgIFMdZ1zjkXR/FMEJOB3pJ6SMogaHQeX2Wd8cDF4euzgXcteLR7PDBSUqakHkBvYFIcY3XOOVdF3M5RwjaFq4EJBLe5Pm5mMyXdAeSb2XjgMeDJsBG6gCCJEK43jqBBuwy4yu9gcs65uuV9MTnnXCNWU19M3r+uc865qDxBOOeci8oThHPOuaiSpg1C0lpg8V5sIgdYV0vhNCR+3I2LH3fjEstxdzOz3GgLkiZB7C1J+dU11CQzP+7GxY+7cdnb4/ZLTM4556LyBOGccy4qTxDfGJPoABLEj7tx8eNuXPbquL0NwjnnXFR+BuGccy4qTxDOOeeiavQJQtIwSXMkzZd0U6LjiSdJj0taI+nLiLI2kt6SNC/82TqRMdY2SV0kvSdptqSZkq4Ny5P9uLMkTZI0PTzu34blPSR9Fh73s2FPy0lHUqqkzyW9Es43luNeJGmGpGmS8sOyPf6sN+oEETFu9slAX2BUOB52snoCGFal7CbgHTPrDbwTzieTMuAGMzsAOBS4KvwdJ/txFwPHmdlAYBAwTNKhBOO+3xce9waCceGT0bXA7Ij5xnLcAMea2aCI5x/2+LPeqBMEEeNmm1kJUDludlIysw8JulWPNAL4V/j6X8AZdRpUnJnZSjObGr7eQvBPoxPJf9xmZlvD2fRwMuA4gvHfIQmPG0BSZ+BU4B/hvGgEx12DPf6sN/YEEW3c7J3Gvk5y7cxsJQT/TIF9EhxP3EjqDgwGPqMRHHd4mWUasAZ4C/ga2GhmZeEqyfp5vx/4BVARzrelcRw3BF8C3pQ0RdIVYdkef9bjP6hp/RbT2Neu4ZOUDbwAXGdmm4MvlcktHGRrkKRWwEvAAdFWq9uo4kvSacAaM5si6ZjK4iirJtVxRzjczFZI2gd4S9JXe7Oxxn4G4WNfw2pJHQDCn2sSHE+tk5ROkBz+bWYvhsVJf9yVzGwj8D5BG0yrcPx3SM7P++HAcEmLCC4ZH0dwRpHsxw2Ama0If64h+FIwlL34rDf2BBHLuNnJLnJc8IuB/yYwlloXXn9+DJhtZvdGLEr2484NzxyQ1AT4LkH7y3sE479DEh63md1sZp3NrDvB3/O7ZnYBSX7cAJKaSWpe+Ro4EfiSvfisN/onqSWdQvANo3Lc7N8nOKS4kfQMcAxBF8CrgduA/wDjgK7AEuAcM6vakN1gSToC+AiYwTfXpH9F0A6RzMc9gKBBMpXgi+A4M7tDUk+Cb9ZtgM+BC82sOHGRxk94iennZnZaYzju8BhfCmfTgKfN7PeS2rKHn/VGnyCcc85F19gvMTnnnKuGJwjnnHNReYJwzjkXlScI55xzUXmCcM45F5UnCOfqiKRWkn6S6Dici5UnCOfqQNhzcCtgtxKEAv536hLCP3jORSHplnCckLclPSPp55Lel5QXLs8Ju3NAUndJH0maGk6HheXHhGNRPE3woN7dwL5hX/33hOvcKGmypC8ixmzoHo5f8VdgKtBF0hOSvgz7+v9Z3b8jrjFq7J31ObcTSUMIumkYTPA3MhWYUkOVNcAJZlYkqTfwDFDZF/9Q4EAzWxj2JnugmQ0K93Mi0DtcR8B4SUcRPO26H3Cpmf0kjKeTmR0Y1mtVm8frXHU8QTi3syOBl8ysEEDSrvrnSgcekjQIKAf6RCybZGYLq6l3Yjh9Hs5nEySMJcBiM5sYli8Aekp6EHgVeHM3j8e5PeIJwrnoovVBU8Y3l2WzIsp/RtC31cBweVHEsm017EPAXWb26LcKgzONHfXMbIOkgcBJwFXAucBlsRyEc3vD2yCc29mHwJmSmoS9Y54eli8ChoSvz45YvyWw0swqgIsIOsiLZgvQPGJ+AnBZOFYFkjqF/fh/i6QcIMXMXgB+DRy0R0fl3G7yMwjnqjCzqZKeBaYBiwl6gwX4EzBO0kXAuxFV/gq8IOkcgm6lo541mNl6Sf+T9CXwupndKOkA4NNwAKOtwIUEl6kidQL+GXE30817fZDOxcB7c3VuFyTdDmw1sz8lOhbn6pJfYnLOOReVn0E455yLys8gnHPOReUJwjnnXFSeIJxzzkXlCcI551xUniCcc85F9f9GLsTOBoZFlwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "regular_Z_shock_path = {\"Z\": dZ[:, 0]}\n", + "\n", + "dC_alt = 100 * rbc_model.solve_impulse_linear(ss, regular_Z_shock_path, unknowns, targets).normalize()[\"C\"][:50]\n", + "\n", + "plt.plot(dC_alt, label='regular shock', linewidth=2.5)\n", + "plt.title(r'Consumption response to TFP shocks')\n", + "plt.ylabel(r'% deviation from ss')\n", + "plt.xlabel(r'quarters')\n", + "plt.show()" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4 Nonlinear solution\n", "\n", - "To obtain nonlinear impulse responses that capture the different scale and sign effects of shocks, we use `nonlinear.td_solve`. Similarly to `get_G` above, it takes in the steady state dict, the model blocks (in arbitrary order), the names of unknown endogenous variables and the names of target equations.\n", + "To obtain nonlinear impulse responses that capture the different scale and sign effects of shocks, we use the `solve_impulse_nonlinear` method of the `rbc_model`. Similarly to `solve_jacobian` above, it takes in the `SteadyStateDict` object, the names of unknown endogenous variables and the names of target equations.\n", "\n", - "However, the names of the exogenous variables would not be sufficient, since we're calculating the nonlinear response to a specific shock. Instead, `td_solve` takes the *sequences* for any exogenous variables that are shocked.\n", + "However, the names of the exogenous variables would not be sufficient information, since we're calculating the nonlinear response to a specific shock path. Instead, `solve_impulse_nonlinear` requires the full *sequences* for any exogenous variables that are shocked.\n", "\n", "So for the news shock above, we can just call: " ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -431,26 +391,23 @@ "output_type": "stream", "text": [ "On iteration 0\n", - " max error for goods_mkt is 7.86E-04\n", " max error for euler is 1.04E-02\n", + " max error for goods_mkt is 7.86E-04\n", "On iteration 1\n", - " max error for goods_mkt is 6.75E-05\n", " max error for euler is 6.68E-05\n", + " max error for goods_mkt is 6.75E-05\n", "On iteration 2\n", - " max error for goods_mkt is 1.27E-07\n", " max error for euler is 3.85E-07\n", + " max error for goods_mkt is 1.27E-07\n", "On iteration 3\n", - " max error for goods_mkt is 1.47E-09\n", - " max error for euler is 3.60E-09\n" + " max error for euler is 3.60E-09\n", + " max error for goods_mkt is 1.47E-09\n" ] } ], "source": [ - "td_nonlin = sj.td_solve(ss=ss, \n", - " block_list=[firm, household, mkt_clearing],\n", - " unknowns=['K', 'L'],\n", - " targets=['goods_mkt', 'euler'],\n", - " Z=ss['Z']+dZ[:, 1])" + "news_Z_shock_path = {\"Z\": dZ[:, 1]}\n", + "td_nonlin = rbc_model.solve_impulse_nonlinear(ss, news_shock_path, unknowns, targets)" ] }, { @@ -462,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 61, "metadata": {}, "outputs": [ { @@ -479,7 +436,7 @@ } ], "source": [ - "dC_nonlin = 100 * (td_nonlin['C']/ss['C'] - 1)\n", + "dC_nonlin = 100 * td_nonlin.deviations().normalize()[\"C\"]\n", "\n", "plt.plot(dC[:50, 1], label='linear', linewidth=2.5)\n", "plt.plot(dC_nonlin[:50], label='nonlinear', linestyle='--', linewidth=2.5)\n", diff --git a/notebooks/two_asset.ipynb b/notebooks/two_asset.ipynb index 4dae356..18543f5 100644 --- a/notebooks/two_asset.ipynb +++ b/notebooks/two_asset.ipynb @@ -56,8 +56,8 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "import sequence_jacobian as sj\n", - "from sequence_jacobian import simple, het, solved" + "from sequence_jacobian import simple, het, solved, create_model\n", + "from sequence_jacobian.models import two_asset" ] }, { @@ -66,105 +66,66 @@ "source": [ "## 1 Calibrate steady state\n", "\n", - "We developed an efficient backward iteration function to solve the Bellman equation in (1). Although we view this as a contribution on its own, discussing the algorithm goes beyond the scope of this notebook. If you are interested in how we solve a two-asset model with convex portfolio-adjustment costs in discrete time, please see appendix B of the paper for a detailed description and `two_asset.py` for the implementation." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "ss = sj.two_asset.two_asset_ss(verbose=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2 Define simple blocks\n", + "We developed an efficient backward iteration function to solve the Bellman equation in (1). Although we view this as a contribution on its own, discussing the algorithm goes beyond the scope of this notebook. If you are interested in how we solve a two-asset model with convex portfolio-adjustment costs in discrete time, please see appendix B of the paper for a detailed description and `two_asset.py` for the implementation.\n", "\n", - "Compare these to the equations in appendix A.3 of the paper." + "To solve for the steady state, we will use the blocks we have set up in the `two_asset.py` module located in `sequence_jacobian/models`, so we will omit the step-by-step discussion of the steady state, since this procedure is repeated and discussed in the other model notebooks." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ - "@simple\n", - "def dividend(Y, w, N, K, pi, mup, kappap, delta):\n", - " psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y\n", - " I = K - (1 - delta) * K(-1)\n", - " div = Y - w * N - I - psip\n", - " return psip, I, div\n", - "\n", - "@simple\n", - "def taylor(rstar, pi, phi):\n", - " i = rstar + phi * pi\n", - " return i\n", - "\n", - "@simple\n", - "def fiscal(r, w, N, G, Bg):\n", - " tax = (r * Bg + G) / w / N\n", - " return tax\n", - "\n", - "@simple\n", - "def finance(i, p, pi, r, div, omega, pshare):\n", - " rb = r - omega\n", - " ra = pshare * (div + p) / p(-1) + (1-pshare) * (1 + r) - 1\n", - " fisher = 1 + i(-1) - (1 + r) * (1 + pi)\n", - " return rb, ra, fisher\n", - "\n", - "@simple\n", - "def wage(pi, w, N, muw, kappaw):\n", - " piw = (1 + pi) * w / w(-1) - 1\n", - " psiw = muw / (1 - muw) / 2 / kappaw * (1 + piw).apply(np.log) ** 2 * N\n", - " return piw, psiw\n", + "blocks = [two_asset.household, two_asset.make_grids,\n", + " two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved,\n", + " two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value,\n", + " two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing]\n", + "two_asset_model = create_model(blocks, name=\"Two-Asset HANK\")\n", "\n", - "@simple\n", - "def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta):\n", - " wnkpc = kappaw * (vphi * N**(1+1/frisch) - muw*(1-tax)*w*N*U) + beta *\\\n", - " (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log)\n", - " return wnkpc\n", + "helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2]\n", "\n", - "@simple\n", - "def mkt_clearing(p, A, B, Bg):\n", - " asset_mkt = p + Bg - B - A\n", - " return asset_mkt" + "calibration = {\"Y\": 1., \"r\": 0.0125, \"rstar\": 0.0125, \"tot_wealth\": 14, \"delta\": 0.02, \"kappap\": 0.1, \"muw\": 1.1,\n", + " \"Bh\": 1.04, \"Bg\": 2.8, \"G\": 0.2, \"eis\": 0.5, \"frisch\": 1, \"chi0\": 0.25, \"chi2\": 2,\n", + " \"epsI\": 4, \"omega\": 0.005, \"kappaw\": 0.1, \"phi\": 1.5, \"nZ\": 3, \"nB\": 50, \"nA\": 70,\n", + " \"nK\": 50, \"bmax\": 50, \"amax\": 4000, \"kmax\": 1, \"rho_z\": 0.966, \"sigma_z\": 0.92}\n", + "unknowns_ss = {\"beta\": 0.976, \"chi1\": 6.5, \"vphi\": 1.71, \"Z\": 0.4678, \"alpha\": 0.3299, \"mup\": 1.015, 'w': 0.66}\n", + "targets_ss = {\"asset_mkt\": 0., \"B\": \"Bh\", 'wnkpc': 0., 'piw': 0.0, \"K\": 10., \"wealth\": \"tot_wealth\", \"N\": 1.0}\n", + "ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver=\"hybr\",\n", + " helper_blocks=helper_blocks,\n", + " helper_targets=[\"wnkpc\", \"piw\", \"K\", \"wealth\", \"N\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 3 Define solved blocks\n", + "## 2 Define solved blocks\n", "\n", - "Solved blocks are mini SHADE models embedded as blocks inside larger SHADE models. Like simple blocks, solved blocks correspond to aggregate equilibrium conditions: they map sequences of aggregate inputs directly into sequences of aggregate outputs. The difference is that in the case of simple blocks, this mapping has to be analytical, while solved blocks are designed to accommodate implicit relationships that can only be evaluated numerically. \n", + "Solved blocks are miniature models embedded as blocks inside of our larger model. Like simple blocks, solved blocks correspond to aggregate equilibrium conditions: they map sequences of aggregate inputs directly into sequences of aggregate outputs. The difference is that in the case of simple blocks, this mapping has to be analytical, while solved blocks are designed to accommodate implicit relationships that can only be evaluated numerically. \n", "\n", "Such implicit mappings between variables become more common as macro complexity increases. Solved blocks are a valuable tool to simplify the DAG of large macro models.\n", "\n", - "### 3.1 Price setting \n", + "### 2.1 Price setting \n", "The Phillips curve characterizes $(\\pi)$ conditional on $(Y, mc, r):$ \n", "\n", "$$\n", "\\log(1+\\pi_t) = \\kappa_p \\left(mc_t - \\frac{1}{\\mu_p} \\right) + \\frac{1}{1+r_{t+1}} \\frac{Y_{t+1}}{Y_t} \\log(1+\\pi_{t+1})\n", "$$\n", "\n", - "Inflation shows up with two different time displacements, and so we could not express it analytically. Instead, we write a function that returns the residual of the equation, and use the decorator `@solved` to make it into a SolvedBlock." + "Inflation shows up with two different time displacements, and so we could not express it analytically. Instead, we write a function that returns the residual of the equation, and use the decorator `@solved` to make it into a `SolvedBlock`." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 13, "metadata": { "scrolled": false }, "outputs": [], "source": [ "@solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver=\"brentq\")\n", - "def pricing(pi, mc, r, Y, kappap, mup):\n", + "def pricing_solved(pi, mc, r, Y, kappap, mup):\n", " nkpc = kappap * (mc - 1/mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \\\n", " (1 + r(+1)) - (1 + pi).apply(np.log)\n", " return nkpc" @@ -174,14 +135,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When our routines encounter a solved block in `block_list`, they compute its Jacobian via the the implicit function theorem, as if it was a SHADE model on its own. Given the Jacobian, the rest of the code applies without modification. " + "When our routines encounter a solved block in `blocks`, they compute its Jacobian via the the implicit function theorem, as if it was a SHADE model on its own. Given the Jacobian, the rest of the code applies without modification. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.2 Equity price\n", + "### 2.2 Equity price\n", "The no arbitrage condition characterizes $(p)$ conditional on $(d, p, r).$\n", "\n", "$$\n", @@ -191,14 +152,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 14, "metadata": { "scrolled": true }, "outputs": [], "source": [ "@solved(unknowns={'p': (10, 15)}, targets=['equity'], solver=\"brentq\")\n", - "def arbitrage(div, p, r):\n", + "def arbitrage_solved(div, p, r):\n", " equity = div(+1) + p(+1) - p * (1 + r(+1))\n", " return equity" ] @@ -207,7 +168,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.3 Investment with adjustment costs\n", + "### 2.3 Investment with adjustment costs\n", "\n", "Sometimes multiple equilibrium conditions can be combined in a self-contained solved block. Investment subject to capital adjustment costs is such a case. In particular, we can use the following four equations to solve for $(K, Q)$ conditional on $(Y, w, r)$.\n", " \n", @@ -240,12 +201,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Solved blocks that contain multiple simple blocks have to be initialized with the `solved_block.solved` function instead of the decorator `@solved`." + "Solved blocks that contain multiple simple blocks have to be initialized with the `solved` function instead of the decorator `@solved`." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -281,67 +242,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4 Determinacy check\n", + "## 3 Dynamics\n", "\n", - "Let's start by defining the inputs common across our convenience functions. Since computing the Jacobian of this two-asset HA block is somewhat costly, it's a good idea to save it for subsequent use." + "As before, we can compute $G$ and calculate impulse responses. To speed up the computation of $G$, we can reuse the pre-computed Jacobian of the household so it is not redundantly computed." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ + "exogenous = [\"rstar\", \"Z\", \"G\"]\n", + "unknowns = [\"r\", \"w\", \"Y\"]\n", + "targets = [\"asset_mkt\", \"fisher\", \"wnkpc\"]\n", "T = 300\n", - "block_list = [sj.two_asset.household, pricing, arbitrage, production, \n", - " dividend, taylor, fiscal, finance, wage, union, mkt_clearing]\n", - "exogenous = ['rstar', 'Z', 'G']\n", - "unknowns = ['r', 'w', 'Y']\n", - "targets = ['asset_mkt', 'fisher', 'wnkpc']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We are ready to apply the winding number criterion. Recall that a winding number of 0 indicates that the model has a unique solution around the steady state, while a winding number of -1 or less indicates indeterminacy." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Winding number: 0\n" - ] - } - ], - "source": [ - "A = sj.get_H_U(block_list, unknowns, targets, T, ss, asymptotic=True, save=True)\n", - "wn = sj.determinacy.winding_criterion(A)\n", - "print(f'Winding number: {wn}')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Linearized dynamics\n", "\n", - "Computing $G$ is fast using the saved HA Jacobian." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "G = sj.get_G(block_list, exogenous, unknowns, targets, T=T, ss=ss, use_saved=True)" + "J_ha = two_asset.household.jacobian(ss=ss, T=T, exogenous=['N', 'r', 'ra', 'rb', 'tax', 'w'])\n", + "G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha})" ] }, { @@ -353,14 +271,14 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 32, "metadata": { "scrolled": true }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -394,7 +312,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 38, "metadata": { "scrolled": true }, @@ -406,33 +324,25 @@ "On iteration 0\n", " max error for asset_mkt is 4.22E-06\n", " max error for fisher is 2.50E-03\n", - " max error for wnkpc is 6.15E-08\n", + " max error for wnkpc is 5.08E-08\n", "On iteration 1\n", - " max error for asset_mkt is 2.79E-04\n", - " max error for fisher is 1.49E-06\n", - " max error for wnkpc is 2.61E-05\n", + " max error for asset_mkt is 2.17E-05\n", + " max error for fisher is 1.56E-06\n", + " max error for wnkpc is 1.97E-05\n", "On iteration 2\n", - " max error for asset_mkt is 8.78E-06\n", - " max error for fisher is 9.99E-08\n", - " max error for wnkpc is 7.67E-07\n", + " max error for asset_mkt is 1.16E-07\n", + " max error for fisher is 3.00E-08\n", + " max error for wnkpc is 8.55E-08\n", "On iteration 3\n", - " max error for asset_mkt is 5.15E-07\n", - " max error for fisher is 2.62E-09\n", - " max error for wnkpc is 2.08E-08\n", - "On iteration 4\n", - " max error for asset_mkt is 3.12E-08\n", - " max error for fisher is 1.39E-10\n", - " max error for wnkpc is 1.03E-09\n", - "On iteration 5\n", - " max error for asset_mkt is 1.92E-09\n", - " max error for fisher is 7.90E-12\n", - " max error for wnkpc is 5.64E-11\n" + " max error for asset_mkt is 1.13E-09\n", + " max error for fisher is 6.92E-11\n", + " max error for wnkpc is 5.40E-10\n" ] } ], "source": [ - "td_nonlin = sj.td_solve(ss, block_list, unknowns, targets,\n", - " rstar=ss['r']+drstar[:,2], use_saved=True)" + "td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {\"rstar\": drstar[:, 2]},\n", + " unknowns, targets, Js={'household': J_ha})" ] }, { @@ -444,14 +354,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 39, "metadata": { "scrolled": true }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEWCAYAAABrDZDcAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nO3dd3wUdfrA8c+zmwYh9IAQSkKVKl1QBDwL2LC3U89yP8tZTk/Pdt5hOa9YzrOcnuKd4nl3lrPcoaJiw44UBUR6CRA6gRAghCS7z++P7yQuMWUD2UySfd6v174y5Tszz0xm59n5zsx3RFUxxhgTvwJ+B2CMMcZflgiMMSbOWSIwxpg4Z4nAGGPinCUCY4yJc5YIjDEmzlkiaERE5Fci8je/4zCmvhCRKSJyr9d9lIgsrcNlq4j0qOV5lq1PbWrUiUBEfiwic0Rkt4hsFJG3RWS033HVBhEZJyI5kcNU9feq+n9+xdQYiMhdIvLPg5j+aBH5SER2ikh2uXEJIvKiiOR5+2JaxLg7ROQXBxF6nRGRGSLS4PYzVf1UVXv7HUd91GgTgYjcCDwM/B5oD3QBngBO9TOuxkBEEvyOoR7bAzwD3FzBuDMABdoC+cCVACKSBZwCPFZHMfrK9p96SFUb3QdoAewGzq6iTDIuUWzwPg8Dyd64cUAOcBOwBdgIXBox7YnAImAXsB74pTf8EuCzcstRoIfXPQWXjN724vscOMRb9g5gCTA4Ytps4HZvWTuAZ4EUIBXYC4S9+ewGOgJ3Af+MmH4i8B2QB8wA+pSb9y+BBcBO4CUgpZJtdYkX65+B7cC93vDLgMVebO8CXb3h4pXd4s17AdA/Yhs8Cbznbb+PS6fzxh8BzPammw0cETFuBvBbL5ZdwHSgrTcuBfgnkOut72ygfcT+8Hfv/7geuBcIVrCeE4AioNjbpvO94R2Bqd66rwAuj2IfPBbILjfsVuBKr/sq4Amv+w1gdBTznOHF/oUX3xtAG+BfuMQyG8g82G3pjR/pLScPmA+M84b/DggBhV4Mf/GGPwKs8+KYCxwVMa+7gFe8/08+8GugAGgTUWYosBVIrGC9S6d/yYv1a+CwiPF9vPXJw+3vEyPGTeH7/XUckBMxrjPwmrfcXOAvuOPCdmBARLl2uO9begWx9cDtwzuBbcBL5b77VwHLcd+RxwHxxgW87bAG9z35B9AiYtrREdt/HXBJBeuTBnwEPFo63wM+Zh7MxPX1g/tClwAJVZS5B5jp/ZPTvY3+24gdpsQrk4g78BcArbzxG0t3dKAVMMTrvoTqE8E2b6dPAT4EVgM/AYK4L/lHEdNmAwu9HbY17ktb4U4d8YX5p9fdC/fr9DhvHW7BHcSSIuY9C3eQa407oF9Vyba6xNse1wEJQBPgNG9+fbxhvwa+8MqPxx0MWuKSQh+gQ8Q22AWMwX3pHindZl4cO4CLvHme7/W38cbPAFZ669bE6/+jN+5K3IGxqbcthwLNvXH/BZ7CJdB23npfWcm6lm3DiGEf4xJ4CjAId+A4ppp9sKJEcBLuYJbk/b0GOB14Nsr9eoa3zbvjktsiYJm3rATcweTZWtiWGbgD44m4A9ZxXn96xLT/Vy62C3FJKQH3A2oT3g8Lb5sWe/tMwFveNOBnEdP/GXisiv9JMXAWbl/+Je57k+h9VgC/8rbrj3D7V++I/e0H3xlvH5nvLTfV+9+O9sY9AdwXsfzrgTcqie0F4A5vvcrmEfHdfxP3Peji7TcTvHGXeXF3A5rhEtLz3rgu3jqc761fG2BQ5Pp4w2aVrttBHzNrYyb17QNcAGyqpsxK4MSI/vF4X1xvh9lLRCLBZe2RXvda3IGnebl5XkL1ieDpiHHXAYsj+gcAeRH92UQcnHFfzJXld+pyX5jSRPAb4OWIcQHcr+FxEfO+MGL8/cCTlWyrS4C15Ya9Dfy03PwLgK64L+My3K/KQLnppgAvRvQ3w/3C7Iw7aM0qV/5Lvv81NAP4dcS4q4F3vO7LcMl8YLnp2wP7gCYRw84nIuFWtg29/s5efGkRw/4ATKlm/6ooEQjwR9wZ0mTcl3keLjn9DvgEdxBKqmSeM4A7Ivr/BLwd0X8KMM/rPphteSveQSli/LvAxRHT/l81678D71e7t00/KTf+XOBzrzuISxwjqvifzCy3r20EjvI+myL3M9zB+a6I/a2iRDAKd2D+wY9F4HDcr/CA1z8HOKeS2P7h/S87VTBO2T8xvAzc5nV/AFwdMa43Ltkl4GoBXq9keVNwVY8LgZur+h/U5NNYrxHkAm2rqYvsiDstK7XGG1Y2D1UtiegvwB20AM7EHZTXiMjHIjKqBrFtjujeW0F/s/2Ls66KGKuy3/qpatibV0ZEmU0R3ZHrV5F15fq7Ao94Fz7zcKfTAmSo6oe40+zHgc0iMllEmlc0L1Xd7U3bsXzMnjVRxvw87mD1oohsEJH7RSTRizMR2BgR61O4g280OgLbVXVXFTFFRZ3bVHWgql4B3IarJhvmfcbiftVeVsVsot1/DmZbdgXOLt1e3jYbDXSoLCgRuUlEFnsXyfNwZyxtI4qU33/+B/QVkW64M46dqjqrsvmz/z4TxlXdlu4z67xhla1nRToDa8p9x0vn/xXubHqsiByKq/6ZWsl8bsHt97NE5DsRKf+/q2wbV3T8ScD9cOmM+6FamZNwZ1VPVlGmRhprIvgSV4d5WhVlNuB2+FJdvGHVUtXZqnoq7mDyX1ymB7fzNC0tJyKH1CDmynSuJEatZrr91k9ExJvX+gOMo/zy1uGqV1pGfJqo6hcAqvqoqg4F+uGqHyIvnpatk4g0w1VjlF6rifyfgFvnamNW1WJVvVtV++Lqxk/GVbmtw50RtI2Is7mq9otyPTcArSPv8Ik2pqqISH8vzsm4M8G56n7yzQYGHsy8PQe8LXHb7Ply/9tUVf2jN36/bSQiR+HOIs7BVZ+2xNWZS0Sx/aZR1ULc9+YC3NnL89XEFLnPBIBOfL/PdPaG1WQ91wFdqvix+Byuuusi4BUv3h9Q1U2qermqdsTVEjwR5S2jFR1/SnCJfR2u+q8yTwPvANNEJDWKZVWrUSYCVd0JTAIeF5HTRKSpiCSKyAkicr9X7AXg1yKSLiJtvfLV3jYoIkkicoGItFDVYtzFr5A3ej7QT0QGiUgK7pT2YF0jIp1EpDWuHvQlb/hmoI2ItKhkupeBk0TkGO+X8U24A+IXtRATuF8jt4tIPwARaSEiZ3vdw0XkcG+5e3BJORQx7YkiMlpEknAXLL9S1XW4euNe3m2/CSJyLtAXV89aJe+2zQEiEsT9T4qBkKpuxF0I/ZOINBeRgIh0F5GxlcxqM5BZemDx4voC+IOIpIjIQOCnuAu0FcUR8P73ia5XUrz1jCwjuLOl671fsquB0u0xFlhV3fpG4YC3Je57cIqIjBeRoLcO40Skkzd+M65uu1Qa7iC2FUgQkUlAc6r3D1y140Sq/+4NFZEzvAP3Dbh9eSZQ+uv9Fu87Pg5XRfZiNfObhate+qOIpHrreGTE+Odx128u9OKskIicHbFdduASXqiy8hFeAH4hIlnej6Hf4y40l+D2rWNF5Bzvf9dGRAaVm/5aYCnwpog0iWJ5VWqUiQBAVR8CbsRdxNyKy7LX4n7Bg7vgMgdXX/st7k6EaB/UuAjIFpF83F0BF3rLXIa7wPw+7k6Bz2phVf6NO5Ct8j73estagtuZVnmn7/tVGanqUi+ux3AXqE8BTlHVolqICVV9HbgPVxWTj6uzPMEb3Rz3q2UH7pQ3F3iw3DrdiasSGor7VYiq5uJ+yd/kTXMLcLKqbosipENwd5bk4y58f8z3B5ef4KpcSu++eoXKqzn+4/3NFZGvve7zgUzcr7jXgTtV9b1Kph+Dq6KZhvuVtxf3/4t0KbBQVed4/a95896Ku27wVNWrWr2D2ZZe8jsV98Oj9LtzM98fLx4BzhKRHSLyKK5K7m3cdaE1uMRfviqoouV8jrvz7WtVza6m+P9w1xVKL4Cf4Z0FFuESyQm4/fwJ4Cfe96OqZYdw34keuGt+Od78S8fn4I4JCnxaxayGA1+JyG5c9dH1qrq6mnUBV8//PO660GrcNrvOW/ZaXNXzTbjvyDzgsHLxK3AFbjv/z/vxccBKb2Uy9ZC4B5L+T1Xf9zuW2iIiU3AX7H7tdyzGfyLyIfBvVa30iXgRuQt3w8WFdRaYW+4zwIZ42FftwQ5jjC9EZDgwhHr4kKeIZOIeABzsbyR1o9FWDRlj6i8ReQ5XhXpDuTuyfCciv8VVdT4QZTVPg2dVQ8YYE+fsjMAYY+Jcg7tG0LZtW83MzPQ7DGOMaVDmzp27TVXTKxrX4BJBZmYmc+bMqb6gMcaYMiJS/knzMlY1ZIwxcc4SgTHGxLmYJgIRmSAiS0VkhYjcVsH4S0Rkq4jM8z4N7q1HxhjT0MXsGoHX5svjuJYFc4DZIjJVVReVK/qSql4bqziMMQ1DcXExOTk5FBZW2L6biVJKSgqdOnUiMTEx6mliebF4BLBCVVcBiMiLuCcIyycCY4whJyeHtLQ0MjMzce3ymZpSVXJzc8nJySErKyvq6WJZNZTB/g1P5VBxG+FnisgCEXlFRDpXMB4RuULcS+jnbN26NRaxGmN8VlhYSJs2bSwJHAQRoU2bNjU+q4plIqjov1n+MeY3cO9YHYh73Py5imakqpNVdZiqDktPr/A2WGNMI2BJ4OAdyDaMZSLIYf+XqpS+SKKMquaq6j6v92lck8S1LhRWlmzK55W5OSzbXK+aNTHGGN/FMhHMBnp6L15IAs6j3OveRCSyTfiJuHbka13+3mI+/stV9PrfyRT99/pYLMIY0wg0a+beJLlhwwbOOussn6OpOzG7WKyqJSJyLe6lFUHgGVX9TkTuAeao6lTg5yIyEfd2o+24txXVulapSQxNWsvA8GrW5dqjE8aYqnXs2JFXXnklpssoKSkhIaF+NO4Q06Oiqk5T1V6q2l1Vf+cNm+QlAVT1dlXtp6qHqerR1b1V6GBsT+sDQIeibCi229OMMZXLzs6mf//+AEyZMoUzzjiDCRMm0LNnT2655ZayctOnT2fUqFEMGTKEs88+m927dwNwzz33MHz4cPr3788VV1xBaSvP48aN41e/+hVjx47lkUceqfsVq0T9SEd1QDscBjtfJoEQ+WsX0Lz7CL9DMsZU4u43vmPRhvxan2/fjs2585R+NZ5u3rx5fPPNNyQnJ9O7d2+uu+46mjRpwr333sv7779Pamoq9913Hw899BCTJk3i2muvZdKkSQBcdNFFvPnmm5xyyikA5OXl8fHHH9fqeh2suEkELboPB+98Y/PSrywRGFOPLdqQz1ert/sdRpljjjmGFi1aANC3b1/WrFlDXl4eixYt4sgj3Tvvi4qKGDVqFAAfffQR999/PwUFBWzfvp1+/fqVJYJzzz234oX4KG4SQffeA9n9ZgrNpJCidd/4HY4xpgp9OzavV/NNTk4u6w4Gg5SUlKCqHHfccbzwwgv7lS0sLOTqq69mzpw5dO7cmbvuumu/+/pTU1MPLPgYiptEkN68Cd8EujFYF5G6/Tu/wzHGVOFAqm/q2siRI7nmmmtYsWIFPXr0oKCggJycHNq1awdA27Zt2b17N6+88kq9vwMpbhIBwLa0QyF/ER32rYRQMQSjb4vDGGMipaenM2XKFM4//3z27XOPQ91777306tWLyy+/nAEDBpCZmcnw4cN9jrR6De6dxcOGDdMDfTHNu//6M+OX3wXArks/Jq3roFqMzBhzMBYvXkyfPn38DqNRqGhbishcVR1WUfm4uqm+We+x3F18EWfvm8SiQmuqwhhjIM6qhnr06ssFoRMAWLB5H4f39jkgY4ypB+LqjKB98xTS09zV/4UbdvocjTHG1A9xlQgA+nu3j3273hKBMcZAnFUNAfyoxSbOT3yYfvnZ7Fk/ldSM/n6HZIwxvoq7M4Lu7ZpxfHAuGZLLpiWz/A7HGGN8F3eJoGufoRRpEIC9a772ORpjTGM0Y8YMTj75ZACmTp3KH//4R58jqlrcVQ11bN2cJdKVPqwiZdu3fodjjGnkJk6cyMSJE2O6jFAoRDAYPODp4+6MQETYlOruG+1QsAzCYZ8jMsbUF9nZ2fTp04fLL7+cfv36cfzxx7N3717mzZvHyJEjGThwIKeffjo7duwAXLPSt956KyNGjKBXr158+umnP5jnlClTuPbaawG45JJL+PnPf84RRxxBt27d9nvnwQMPPMDw4cMZOHAgd955Z9nw0047jaFDh9KvXz8mT55cNrxZs2ZMmjSJww8/nC+//PKg1jvuzggAitsNhNVvk0oBe7esoMkhvfwOyRhT3jf/gnn/rrrMIQPghIhql40L4J3bKy476Mcw+IJqF7t8+XJeeOEFnn76ac455xxeffVV7r//fh577DHGjh3LpEmTuPvuu3n44YcB94KZWbNmMW3aNO6++27ef//9Kue/ceNGPvvsM5YsWcLEiRM566yzmD59OsuXL2fWrFmoKhMnTuSTTz5hzJgxPPPMM7Ru3Zq9e/cyfPhwzjzzTNq0acOePXvo378/99xzT7XrVJ24TASpmUNhteveuOQrulkiMKb+yVsLaz6r2TSFOyufJnN0VLPIyspi0CDX/MzQoUNZuXIleXl5jB07FoCLL76Ys88+u6z8GWecUVY2Ozu72vmfdtppBAIB+vbty+bNmwH3gpvp06czePBgAHbv3s3y5csZM2YMjz76KK+//joA69atY/ny5bRp04ZgMMiZZ54Z1TpVJy4TQdc+wyj5MECChClYMxe4yO+QjDHltewCXas5eB8yYP/+lBaVT9OyS1SLLd/kdF5eXlTlS5unrsn8S9t6U1Vuv/12rrzyyv3Kzpgxg/fff58vv/ySpk2bMm7cuLImrVNSUg7qukCkuEwEGemtWS6d6MVakrbYBWNj6qXBF0RVlbOfDgPh0rdqNYwWLVrQqlUrPv30U4466iief/75srOD2jJ+/Hh+85vfcMEFF9CsWTPWr19PYmIiO3fupFWrVjRt2pQlS5Ywc+bMWl1uqbhMBCLCu60v4C+bdrGn+QD+7ndAxph67bnnnuOqq66ioKCAbt268eyzz9bq/I8//ngWL15c9oazZs2a8c9//pMJEybw5JNPMnDgQHr37s3IkSNrdbml4qoZ6kh/mLaYpz5ZRUJAWHj3eFISa+cUyxhzYKwZ6tpjzVBHqX+Ge/9oSVhZummXz9EYY4x/4j4RgDVAZ4yJb3F5jQCga+um/CJ5KkfpHBK/7AQj/+d3SMbEPVVFRPwOo0E7kOr+uD0jCASEoU02MySwgoz8+X6HY0zcS0lJITc394AOZMZRVXJzc0lJSanRdHF7RgCwt21/yPmI1rqDoh0bSGrV0e+QjIlbnTp1Iicnh61bt/odSoOWkpJCp06dajRNXCeCJl2GQI7r3rj4S7oeUTtP6Rljai4xMZGsrCy/w4hLcVs1BNCxz4iy7vzVB39LqjHGNERxnQi6ZmSwVtsDENxsTxgbY+JTTBOBiEwQkaUiskJEbqui3FkioiJS4cMOsRIMCDkprsG59N1L6nLRxhhTb8QsEYhIEHgcOAHoC5wvIn0rKJcG/Bz4KlaxVKWgTT8A0sNbKc7f4kcIxhjjq1ieEYwAVqjqKlUtAl4ETq2g3G+B+4HCGMZSqeTOQ8q6Ny7xJRcZY4yvYpkIMoB1Ef053rAyIjIY6Kyqb8Ywjiod0mcUvyv+MecX3cHccE+/wjDGGN/E8vbRih4PLHtSREQCwJ+BS6qdkcgVwBUAXbpE16Z4tLI6d+L5wEQKi8P02lzC6bU6d2OMqf9ieUaQA3SO6O8EbIjoTwP6AzNEJBsYCUyt6IKxqk5W1WGqOiw9Pb1Wg0wIBujToTkACzfk1+q8jTGmIYhlIpgN9BSRLBFJAs4DppaOVNWdqtpWVTNVNROYCUxU1Tq/ob9/R9cA3aIN+YTC9ni7MSa+xCwRqGoJcC3wLrAYeFlVvxORe0RkYqyWeyBGttzJ44kP845cx8Z50/0Oxxhj6lRMm5hQ1WnAtHLDJlVSdlwsY6lKj4y29A7OAmDBytl0GjLer1CMMabOxfWTxaWyMnuwVb33E2xc4G8wxhhTxywRAEmJQdYkuVtHW+cv9jkaY4ypW5YIPPmt3EPPHYvXES60V1caY+KHJQJPQsZgAAKibFxmLZEaY+KHJQJPeq/vm6TOXT7Lx0iMMaZuWSLwdOtxKDu0GQC60V5daYyJH5YIPMmJCWQn9gCgZd4in6Mxxpi6E9evqixvfsdzeX7FSFbRk9dVEamouSRjjGlc7IwgQrDPibwWHsO8wg6s3V7gdzjGGFMnLBFE6J/Roqx74XprgM4YEx+qTQQicqSIpHrdF4rIQyLSNfah1b0+HZoTDLjqoG/X7/Q5GmOMqRvRnBH8FSgQkcOAW4A1wD9iGpVPUhKD3NDiE15M+i2nzLvS73CMMaZORJMISlRVca+ZfERVH8G9S6BRGtgkl5GBxfTctxAt2ed3OMYYE3PRJIJdInI7cCHwlvdS+sTYhuWjjocBkEQJW1bZ8wTGmMYvmkRwLrAP+KmqbsK9d/iBmEblozY9hpd1b1lqTxgbYxq/aJ4j2IWrEgqJSC/gUOCF2Ibln6zeg9irSTSRIkpy5vkdjjHGxFw0ZwSfAMkikgF8AFwKTIllUH5KbZLMqoRuADTL+87naIwxJvaiSQSiqgXAGcBjqno60C+2Yflre9qhAHTetwINFfscjTHGxFZUiUBERgEXAG95w4KxC8l/2sFdME6hiNxsOyswxjRu0SSC64Hbgde9l893Az6KbVj+atn9+yapNy39ysdIjDEm9qq9WKyqn+CuE5T2rwJ+Hsug/JbVZwgPvH4O32kmIxhCf78DMsaYGLLWRyuQltqUaa0uYPW2PQS3+B2NMcbEljU6V4nSBugWbrA2h4wxjZslgkr079gcgM35+9iyq9DnaIwxJnaqrRoSkSzgOiAzsryqToxdWP4b3KaEPyc+Tn/JZsuXt9Du+Ev8DskYY2IimmsE/wX+DrwBhGMbTv3Ru0tHBgdmkigh5q6dA1zid0jGGBMT0SSCQlV9NOaR1DMtmqexPNCZnppN01x7lsAY03hFkwgeEZE7gem4xucAUNWvYxZVPbE17VB65meTsXcZqIK9w9gY0whFkwgGABcBP+L7qiH1+hu1knYDIf8dmrObvI0radmxh98hGWNMrYsmEZwOdFPVolgHU980zxoGK1z3+sUzLREYYxqlaG4fnQ+0PJCZi8gEEVkqIitE5LYKxl8lIt+KyDwR+UxE+h7IcmKlS98RhNVVB+1d0+hrwowxcSqaM4L2wBIRmc3+1wiqvH3Ue5PZ48BxQA4wW0SmquqiiGL/VtUnvfITgYeACTVbhdhp3aoVqwMZZGkOKdu+9TscY4yJiWgSwZ0HOO8RwAqvbSJE5EXce4/LEoGq5keUT8Vde6hXNqf2Jmt3Dh0LltoFY2NMoxRNo3Mfi0h7oPQdjrNUNZoWeDKAdRH9OcDh5QuJyDXAjUASlVyAFpErgCsAunTpEsWia8/GzDO57ZssFoYz+VdBMS1Sk+p0+cYYE2vVXiMQkXOAWcDZwDnAVyJyVhTzruin8w9+8avq46raHbgV+HVFM1LVyao6TFWHpaenR7Ho2tOy/3G8GPoRC7Ub323Mr34CY4xpYKKpGroDGF56FiAi6cD7wCvVTJcDdI7o7wRsqKL8i8Bfo4inTpU2Pgfw7fqdHNGjrY/RGGNM7YvmrqFAuaqg3Cinmw30FJEsEUkCzgOmRhYQkZ4RvScBy6OYb51KT0umffNkABav2+pzNMYYU/uiOSN4R0TeBV7w+s8FplU3kaqWiMi1wLu4V1s+473h7B5gjqpOBa4VkWOBYmAHcPGBrESsndM2m6F7/8GQFSsoKVxOQkqq3yEZY0ytieZi8c0icgYwGlfvP1lVX49m5qo6jXJJQ1UnRXRfX7Nw/TGyYwJHbpgPwJKZb3DouPN8jsgYY2pPlVU8IhIUkfdV9TVVvVFVfxFtEmhM+o4+jQJ11UOFC/7rczTGGFO7qkwEqhoCCkSkRVXlGrtWLVuysOkwALpt/wQtibvWNowxjVg0F30LgW9F5O8i8mjpJ9aB1Tf7epwEQHP2sGrOuz5HY4wxtSeaRPAW8BvgE2BuxCeu9Bp9FsUaBGDnN3FXO2aMacQqvVgsIh+o6jFAX1W9tQ5jqpfat2/P18mDGFI0l65bPkTDISQQ9DssY4w5aFWdEXQQkbHARBEZLCJDIj91FWB9sjvLtYfXRneQ8+0nPkdjjDG1o6rbRycBt+GeCH6o3Li4eDFNeZlHnkN4ye8JEWD5t7PofNjRfodkjDEHrdJEoKqvAK+IyG9U9bd1GFO91aVLJnc0u5M3cjPovL1D/GVCY0yjVO3FYksC+2s76CTySeW7Dfms217gdzjGGHPQorlryEQY3++Qsu53v9vkYyTGGFM7LBHUUJ8OaXRp3ZR08tg950W/wzHGmIMWTaNzpa+dbB9ZXlXXxiqo+kxEuKX9HE7c8zsCO5XctafTpksfv8MyxpgDFs2Laa4DNgPv4R4uewt4M8Zx1WtZg8YSEPeOnbWfv+xzNMYYc3CiqRq6Huitqv1UdYD3GRjrwOqzPv2HsYaOAKStftvnaIwx5uBEkwjWATtjHUhDEggGyG7nbh7tUbSY/C1xWUtmjGkkokkEq4AZInK7iNxY+ol1YPVdi8FnlHWv+uwlHyMxxpiDE00iWIu7PpAEpEV84lq/4WPZRBsAkpdX+8I2Y4ypt6J5Q9ndACKS5np1d8yjagASExJY3nosh2x/jZ4F89ibt5UmLdP9DssYY2osmruG+ovIN8BC4DsRmSsi/WIfWv2XMuBUABIkzIrP/uNzNMYYc2CiqRqaDNyoql1VtStwE/B0bMNqGAYccQLrNZ23QiP4ZEtTv8MxxpgDEs0DZamq+lFpj6rOEJHUGMbUYKQkJ/O7Hv9m2ndbab42gctLwiQl2MPaxpiGJaq7hkTkNyKS6X1+DayOdWANxfgBGQDkF5Ywc1Wuz9EYY0zNRZMILgPSgdeA173uS2MZVENy9KHtSAwKAO9YI3TGmCi9DeUAAB2mSURBVAYomruGdgA/r4NYGqTmKYmc1FVJX/Mmxy5YSOik9wgmpfgdljHGRK2qdxY/rKo3iMgbuDeS7UdVJ8Y0sgbk7HbrOXLDv0Fh2axp9Bp9RvUTGWNMPVHVGcHz3t8H6yKQhuzQMWey75vbSZYS9sx7HSwRGGMakEqvEajqXK9zkKp+HPkBBtVNeA1Dm9ZtWJgyFIDMbTPQUInPERljTPSiuVh8cQXDLqnlOBq8Pd1OAKAV+WTP+9DnaIwxJnqVJgIROd+7PpAlIlMjPh8Bdp9kOT2OOouQuruHdsx51edojDEmelVdI/gC2Ai0Bf4UMXwXsCCWQTVEHTt2Zn7iAA4rWUCnTe+DKoj4HZYxxlSrqmsEa1R1hqqOKneN4GtVjaoSXEQmiMhSEVkhIrdVMP5GEVkkIgtE5AMR6XowK+O3nV2PB6CdbiNn0Rc+R2OMMdGJptG5kSIyW0R2i0iRiIREJD+K6YLA48AJQF/gfBHpW67YN8Aw741nrwD313wV6o8uR5xT1r35K6seMsY0DNG0NfQX4DzgP8Aw4CdAjyimGwGsUNVVACLyInAqsKi0QGQbRsBM4MLowq6fMrv3ZkrS+Xy6uyP5BaOx9kiNMQ1BVC2kqeoKIKiqIVV9Fjg6iskycK+5LJXjDavMT4EKXwAsIleIyBwRmbN169ZoQvbN1qE38EF4KLNz9rJx516/wzHGmGpFkwgKRCQJmCci94vIL4BoWh+t6ErpD55QBhCRC3FnGw9UNF5VJ6vqMFUdlp5ev1/+Mr7fIWXd07/b7GMkxhgTnWgSwUVAELgW2AN0Bs6MYrocr2ypTsCG8oVE5FjgDmCiqu6LYr712oCMFnRs4doamj3Pbq4yxtR/0TQ6t8br3AvcXYN5zwZ6ikgWsB53neHHkQVEZDDwFDBBVbfUYN71lohwZZccRix9gD5b1pG34Wtaduzud1jGGFOpqh4oe9n7+613e+d+n+pm7N1iei3wLrAYeFlVvxORe0SktMG6B4BmwH9EZJ6ITD3oNaoHhvTKok/AXR5Z/dlLPkdjjDFVq+qM4Hrv78kHOnNVnQZMKzdsUkT3sQc67/qs75DRbHgrnY5spenKt4Ff+R2SMcZUqqoHyjZ6nWcAJd4DZmWfugmvYQoGA6xs426s6ln4LXu2b6xmCmOM8U80F4ubA9NF5FMRuUZE2sc6qMYgdfDpAAREWfHpyz5HY4wxlas2Eajq3araD7gG6Ah8LCLvxzyyBq7fiGPZpi0ASFj6ls/RGGNM5aJ6oMyzBdiEa3m0XWzCaTySk5JY2nIMAL32zKFw1w6fIzLGmIpF09bQz0RkBvABriXSy722gUw1Evu7m6MSJcSKz1/zORpjjKlYNG0NdQVuUNV5sQ6msel35Mnkf9aUEgIsW5NDf78DMsaYCkRzjeA2oJmIXAogIuneQ2KmGqlNm/JAp78wfN9fuXvTKHbvs1dYGmPqn2iqhu4EbgVu9wYlAv+MZVCNyREjjyBEkJ17i3nms9V+h2OMMT8QzcXi04GJuHaGUNUNQFosg2pMxvc7hL4dmgPw9CeryNvT4JtTMsY0MtEkgiJVVbyWQ0UkmpZHjScQEG4e35sMtvKb0OOsfu4Kv0Myxpj9RHOx+GUReQpoKSKXA5cBT8c2rMZlXO90HmzxMqP2fU7J5gC52Qtpk2mXjo0x9UM0F4sfxL1G8lWgNzBJVR+LdWCNiYjQ9Pg7CKuQIGE2/HdS9RMZY0wdifYNZe+p6s2q+ktVfS/WQTVGhw09kpmp4wAYkPcBm5bO9jcgY4zxVNUM9S4Rya/sU5dBNhatTryTEnWbPPdNOyswxtQPVbU+mqaqzYGHgdtw7xvuhLuV9N66Ca9x6dN/MF82nwBAv11fsG7Bxz5HZIwx0VUNjVfVJ1R1l6rmq+pfie5VlaYCGafeyT511+j3vH2Xv8EYYwzRJYKQiFwgIkERCYjIBUAo1oE1Vt16HMpXrU8F4NC9X7N61rRqpjDGmNiKJhH8GDgH2Ox9zqbcu4dNzWSdMYkCTeazUD+e/SbP73CMMXEumpfXZwOnxj6U+NG5cyYP9P0nj39TDKvhlOztDM9s7XdYxpg4VZP3EZhadNGEMSQnuM3/wLtLcQ9vG2NM3bNE4JNDWqTwk1FdAZi1Opev5n/nc0TGmHhlicBHPxvXgx8lLWZq0q/pNPVcNFTsd0jGmDgUdSIQkZEi8qGIfC4ip8UyqHjROjWJn3TbzcDAajqFc1j49mS/QzLGxKGqniw+pNygG3HNUU8AfhvLoOLJ0LNuYjPuQnH63IcJFRX6HJExJt5UdUbwpIj8RkRSvP483G2j5wLWxEQtSWuWxtJeVwFwiG7h2zce9TkiY0y8qaqJidOAecCbInIRcAMQBpoCVjVUi4affj05tAeg87dPULR3t88RGWPiSZXXCFT1DWA80BJ4DViqqo+q6ta6CC5eNGmSQvaAnwPQhh0sfP0BnyMyxsSTqq4RTBSRz4APgYXAecDpIvKCiHSvqwDjxYhTrmS1dAag+7K/Ubhrh88RGWPiRVVnBPfizgbOBO5T1TxVvRGYBPyuLoKLJ0lJiWwaehMALdjNwtf+4HNExph4UVUi2Ik7CzgP2FI6UFWXq+p5sQ4sHo044WK+DfZlcslJ3JI9nF2F9lyBMSb2qkoEp+MuDJdwgI3MicgEEVkqIitE5LYKxo8Rka9FpEREzjqQZTQmwWCAnFNf4fclF7Bqb1P+/tlqv0MyxsSBqu4a2qaqj6nqk6pa49tFRSQIPA6cAPQFzheRvuWKrQUuAf5d0/k3VhMGdGRARgsA/vbpanbsKfI5ImNMYxfLJiZGACtUdZWqFgEvUq4VU1XNVtUFuNtSDe5F9zcd38t178vn/f8+53NExpjGLpaJIANYF9Gf4w2rMRG5QkTmiMicrVsb/52rY3ulc90hi/g0+XpOXXYrW9ct8zskY0wjFstEIBUMO6C2llV1sqoOU9Vh6enpBxlW/ScijB89gpayhyQJse71O/0OyRjTiMUyEeQAnSP6OwEbYri8RqX/sLHMbjIagMNy32bZl2/4HJExprGKZSKYDfQUkSwRScLdhjo1hstrdFqedDf7NJGgKB3evYINy+f5HZIxphGKWSJQ1RLgWuBdYDHwsqp+JyL3iMhEABEZLiI5uPcgPyUi9naWCD37D2POYPfsXhoF8O9zyN9qJ1XGmNolDe0VicOGDdM5c+b4HUad+vhvNzM2x72rYFlSHzJv/JCklKY+R2WMaUhEZK6qDqtonL2hrAE46tL7mJl2PAC9ihbz5dPX2zuOjTG1xhJBAxAIBhh09XMsTOzPrHBvblj/I56YsdLvsIwxjYQlggYipUlTDrnidW5t+lt20JwH3l3KG/PteoEx5uBZImhA2qa3Y/KlR5CWkgDATf+Zz/wl9rCZMebgWCJoYHq2T+OvFwwlMaD8nBfo+uLRrF+xwO+wjDENmCWCBmh0z7Y8NWYf1yb8j5bsRv91Dvm5m/0OyxjTQFkiaKB+NOFMvsi4FIBOupH1T51BUeFen6MyxjRElggasJGX/Yk5zY4GoE/RQhb89WI0bA25GmNqxhJBAxYIBul/zb9YktAHgGE73+Wr5273OSpjTENjiaCBS2mSSvoVr7JB2gMwcs2TzHlzss9RGWMaEksEjUCbdhkUn/si+bhmJwbOvp2F38z0OSpjTENhiaCR6HroENYd8yTFGuRvoRP5ydQ8srft8TssY0wDYImgEel31Km8d/RU7i85j+17Q1w2ZTZ5BfbOY2NM1SwRNDInjhvNNUd3B2DVtj1c/Y8v2VtgZwbGmMpZImiEbjquNycP7EBLdnH9hlvY+OAols+3awbGmIpZImiEAgHhwbMP4+HWr3J4YAndwmvo+tpJfPb83YRCIb/DM8bUM5YIGqmUxCBH/eJfzO56OSEVkqSE0SsfYuF9x7J+7Sq/wzPG1COWCBqxYGISwy99kNUTXyl7zuCwoq9p+vcxzHxrir3cxhgDWCKICz2GHkvLG2fydasJALSSXYycfT1fPnwBO/PyfI7OGOM3SwRxomlaa4Zc/xLfjnqYfFIByNgxmzOe+IwvVm7zOTpjjJ8sEcSZAeMvpfjyz1iYMpRfFF/Nynzhgr99xR+mLWZfiV1INiYeWSKIQ20yutHv1g8449QzSEkMoApPfbKKKX+6hVXLF/kdnjGmjlkiiFMiwoUju/LWz49iQEYLjg/M5sq9fyP9nz/i41f+QjhsF5KNiReWCOJc9/RmvPqzI7gsawcAabKXsQvv4KsHT2frFnvrmTHxwBKBISkhwMjLH2HZMX9jB80BGFXwESVPHMFHL/6ZHTvzfY7QGBNLlghMmV5HnU3CdTNZ1GwkAB3YxtFL7oKH+vLhX65m0aq1/gZojIkJSwRmP2ltMuh70zssHHQnudIKcM8dDN76P06fPJcznvic/81bT1GJvRLTmMZCGtrTpcOGDdM5c+b4HUZc0JJ9LJ/xAsE5k/lgTxa/L/5x2bgBqTu5vutq+p9wJYekt/UxSmNMNERkrqoOq3CcJQITjQ3bd/Hv2Rt4YdZacvcUcXvCv7gy4S12aRNmtzqB1uOu4bDDhiIifodqjKmAJQJTa/aVhJi2YAMDp51K95KV+42bnTCE3Yf9lBHHnUNqSpJPERpjKuJbIhCRCcAjQBD4m6r+sdz4ZOAfwFAgFzhXVbOrmqclgnoiHCL7y1cp+uJJeu2Zu9+otdqexZ3PIX3M5fTv1pmkBLsUZYzffEkEIhIElgHHATnAbOB8VV0UUeZqYKCqXiUi5wGnq+q5Vc3XEkH9k7d2IeveeYTuG6bSlMKy4Uft+zNbEzowtGsrDs9qw+iOQr+eWSQnBH2M1pj45FciGAXcparjvf7bAVT1DxFl3vXKfCkiCcAmIF2rCMoSQf0V2ruT5dMn0+LbKYSL93LkvkcBd80gmSIWJF/OJlqzMnUwRZ1G0W7AMfQ9tC8piZYYjIm1qhJBQgyXmwGsi+jPAQ6vrIyqlojITqANsF9zmCJyBXAFQJcuXWIVrzlIwSYtOPTUm+GUm9iWs4xHtqfx1ertzFyVS/q2xSRLMV3ZTNeCd2DZO7DsTtZqO1Y2HURhpyNI738M/fv2s8RgTB2LZSKo6PaR8r/0oymDqk4GJoM7Izj40ExMBQK07XIop3aBUwdlALBtfScWf76H4LrP6bxrPk28KqQusoUue6fD8umElt3N0Jf/Ts/OHeif0YJurZPplZLHIV16ktE6jYSgXWswJhZimQhygM4R/Z2ADZWUyfGqhloA22MYk/FJ24wetD3nTtcTKiZv1Ww2zX+fwNrP6ZQ/n6bsZaFmkRdKYXb2DmZn76C7rOeD5Jsp1iDrSGdzQgb5TTtT0iKLxPQepGX05pDOPclo25xESxLGHLBYJoLZQE8RyQLWA+cBPy5XZipwMfAlcBbwYVXXB0wjEUykZc8jaNnzCNcfKmHn6jnsW7uJS3ZnMmv1dlZt203XkGv0LlFCZLGJrNAm2DUXduF+QnwDxRpkUPHfaduqJZltUsls05SRJbNJTmtNkxbpNG3VnhYt02mV1oTmKQn2nIMxFYhZIvDq/K8F3sXdPvqMqn4nIvcAc1R1KvB34HkRWYE7EzgvVvGYeiyYQIseIxnRA0Z4g8JhZduGXqxc2IZ9W5YT2LGaZnvW0LZoPSnsK5t0B2nsCSexJ7eANbkFfE0Bd6fcsN/swyrsJJXVpLFLmlOQ0ILnW11DuHlnWqUm0To1kT77FtCsSQopaW1IbNKMpJRUkpqm0iSlGSkpiTRJDNIkMWjVU/WYhkOESkoIIYQJElIlFFbCe3cSLi5AS0KEwiWEQyEIlxAOlRAKhdBwCcVJLSlq2oGwN01g90aS8lYTDpeg4TAaLkFDJaiG0FAI1TDFwVS2tB/tlqFKQuF2Dln/HoRDqJZAOAQaQsNh0DASDqHhEF93/gklgeSy6UZmP0FCaK9XJgwact3qukVDfNbmbHJSetMsJYHfnNy31redPVBmGhZVdNcmduQsIW/dErbv2sMHzU5mTe4eVm8rIDX3W14J3F7tbI4ofJQNfN80xqdJ19M5sLXCsvs0kb0kcUfxT5kuo0jxksJl/I/BuoiSYAolgSaEg0loIBENBFFJgEAC65r2ZWnLMQQDQmIwQMfCFXTb/TUaSEACQQgkIsEgBIKIBAChOKEZ69LHAO69EUnF+XTO/bys3+twF9hEAAENs7b9jwhJEqqKApkb3yEQ2ocQBlXQMKBoWIEQKKxrPYq8lE6EwhBWpfu2D2m5d40rGw6VHYwiD0xrmg5gYfMxqLoDWdauuQza+T4SDiOEIg5gYQLe323BdJ5reTXhMIRUaVe0np/lP0RQ3WE7oGF3+NYQQoiAhgkQ5rKkB8nXpoRUUVVeK7mGluwuGx90h3wS5Pu2r64suoF3wyPK+p9KfIjxwaqPGc+WjOfukovL+i8Lvs2kxOernGZxuDMnFN1X1t9H1vB2cvX73mGFk9lJs7L++cn/RwspqHKa0nVKT0tm9h3HVruMivh115AxtU8Ead6B1n070Lrv0XQDIvds3TeYPRtHsGfHFgp2bqEofyslu3NhTy6BvdtJKNpOctFOurfrQmphgB0FRewoKKaV7Kp0kclSTDLFKFAcUopDJewqLKFz4hIOD86BksrD/cfm43ixJKus/8LgR1yR+GyVq7g03Ikri9LL+nvJOqZHcYAZXPhkWTPiAN8k30sr2V3lND8rup63w9/fzPfXxFcYE5xd5TQrS7bxQklmWf/FwXmMSnyrymmWhjvx+bbcsv7espV+yd9VOQ3A1p272RHRNmaz5D00lz0V32biCbJ/g4ihKNrWLD9NyQFME81yAALlpttFUwKquNQm3t8AYSntDpLSJJX2wWTapCZHtYyaskRgGhVJbkZq5jBSM6suF/lbLxxWCla/waa8rRTuyqVk3x7CRXsJ7StAiwrQ4gIoLuCo9JFkJnenoChEYXGIlPXdWLN7BwnhQpLC+0jUfQQJEdCQ9/UNkZKcTLsmrhqgJKykxvDNb4FyN9xFsyQpVypcwcEspN4BSdyBKjExibYpyQQEAiIkhZuzLdSK73+jl5YNoOKGbU/qwNAOrQiKIALtw8UsyjuMsARRAqgEXbe4bpUASJATe3ehOLEZwYAQEGHhxtNI1kJUgiABNJAA4s6o3N8Ao9OPYkCz7gQDLr7g9kuYWXicO+sKuOlcdwISDCKSQFZaF55s1ZeAQDAgNCnsxPzdxxLwztwkEESCrnwgkEAgEITEZN5pmUVQhEBACIRLWF94vCuTUFougUAwQDCQgCQECQYS+DIYJBAIlC1L5KRq/0+PRPG/PBhWNWRMXSopguICV38cLiEUKqakpJhwSTFh77uokkC4ZRfKvprF+5Cda8uqfEBdFZnXrxpGJEC4dXd3cBNxB+md60BcdZJIwA0PuAOsiIAEkJQ0JCHFO9CChIpcNVLpAVMCXvWTaeisasiY+iIhyX08Qe9TpSaJ0PwALhA27V7zaRJiU/Vg6je7BcIYY+KcJQJjjIlzlgiMMSbOWSIwxpg4Z4nAGGPinCUCY4yJc5YIjDEmzjW4B8pEZCuw5gAnb0u5l97UExZXzVhcNVdfY7O4auZg4uqqqukVjWhwieBgiMicyp6s85PFVTMWV83V19gsrpqJVVxWNWSMMXHOEoExxsS5eEsEk/0OoBIWV81YXDVXX2OzuGomJnHF1TUCY4wxPxRvZwTGGGPKsURgjDFxrlEmAhGZICJLRWSFiNxWwfhkEXnJG/+ViGTWQUydReQjEVksIt+JyPUVlBknIjtFZJ73mRTruLzlZovIt94yf/DWH3Ee9bbXAhEZUgcx9Y7YDvNEJF9EbihXps62l4g8IyJbRGRhxLDWIvKeiCz3/raqZNqLvTLLReTiisrUYkwPiMgS7//0uoi0rGTaKv/nMYrtLhFZH/H/OrGSaav8/sYgrpciYsoWkXmVTBuTbVbZsaFO9y8tfdNRI/ng3vOxEugGJAHzgb7lylwNPOl1nwe8VAdxdQCGeN1pwLIK4hoHvOnDNssG2lYx/kTgbdybYkcCX/nwP92EeyDGl+0FjAGGAAsjht0P3OZ13wbcV8F0rYFV3t9WXnerGMZ0PJDgdd9XUUzR/M9jFNtdwC+j+F9X+f2t7bjKjf8TMKkut1llx4a63L8a4xnBCGCFqq5S1SLgReDUcmVOBZ7zul8BjhGJ7fv4VHWjqn7tde8CFgMZsVxmLToV+Ic6M4GWItKhDpd/DLBSVQ/0ifKDpqqfANvLDY7cj54DTqtg0vHAe6q6XVV3AO8BE2IVk6pOV9USr3cm0Kk2llVTlWyvaETz/Y1JXN4x4BzghdpaXpQxVXZsqLP9qzEmggxgXUR/Dj884JaV8b40O4E2dRId4FVFDQa+qmD0KBGZLyJvi0i/OgpJgekiMldErqhgfDTbNJbOo/Ivpx/bq1R7Vd0I7ssMtKugjJ/b7jLcmVxFqvufx8q1XrXVM5VUdfi5vY4CNqvq8krGx3yblTs21Nn+1RgTQUW/7MvfIxtNmZgQkWbAq8ANqppfbvTXuOqPw4DHgP/WRUzAkao6BDgBuEZExpQb7+f2SgImAv+pYLRf26smfNl2InIHUAL8q5Ii1f3PY+GvQHdgELARVw1Tnm/7GnA+VZ8NxHSbVXNsqHSyCobVeHs1xkSQA3SO6O8EbKisjIgkAC04sNPYGhGRRNw/+l+q+lr58aqar6q7ve5pQKKItI11XKq6wfu7BXgdd3oeKZptGisnAF+r6ubyI/zaXhE2l1aReX+3VFCmzredd8HwZOAC9SqSy4vif17rVHWzqoZUNQw8XckyfdnXvOPAGcBLlZWJ5Tar5NhQZ/tXY0wEs4GeIpLl/Zo8D5harsxUoPTq+lnAh5V9YWqLV//4d2Cxqj5USZlDSq9ViMgI3P8nN8ZxpYpIWmk37mLjwnLFpgI/EWcksLP0lLUOVPorzY/tVU7kfnQx8L8KyrwLHC8irbyqkOO9YTEhIhOAW4GJqlpQSZlo/uexiC3yutLplSwzmu9vLBwLLFHVnIpGxnKbVXFsqLv9q7avgNeHD+4ul2W4uw/u8Ibdg/tyAKTgqhpWALOAbnUQ02jcKdsCYJ73ORG4CrjKK3Mt8B3uTomZwBF1EFc3b3nzvWWXbq/IuAR43Nue3wLD6uj/2BR3YG8RMcyX7YVLRhuBYtyvsJ/irit9ACz3/rb2yg4D/hYx7WXevrYCuDTGMa3A1RmX7mOld8d1BKZV9T+vg+31vLf/LMAd5DqUj83r/8H3N5ZxecOnlO5XEWXrZJtVcWyos/3Lmpgwxpg41xirhowxxtSAJQJjjIlzlgiMMSbOWSIwxpg4Z4nAGGPinCUCY2qZiLQUkav9jsOYaFkiMKYWiUgQaIlr4bYm04mI2PfR+MJ2PBPXROQOr+3790XkBRH5pYjMEJFh3vi2IpLtdWeKyKci8rX3OcIbPs5rT/7fuAem/gh099qtf8Arc7OIzPYaXLs7Yn6LReQJXLtJnUVkiogsFNfu/S/qfouYeJTgdwDG+EVEhuKaMBiM+y58DcytYpItwHGqWigiPXFPqQ7zxo0A+qvqaq8Fyf6qOshbzvFAT6+MAFO9BsvWAr1xT4Ne7cWToar9vekqfKmMMbXNEoGJZ0cBr6vXJo+IVNemTSLwFxEZBISAXhHjZqnq6kqmO977fOP1N8MlhrXAGnXveAD3UpFuIvIY8BYwvYbrY8wBsURg4l1FbayU8H21aUrE8F8Am4HDvPGFEeP2VLEMAf6gqk/tN9CdOZRNp6o7ROQw3MtGrsG9JOWyaFbCmINh1whMPPsEOF1EmngtS57iDc8GhnrdZ0WUbwFsVNeM8kW41ypWZBfulYOl3gUu89qbR0QyROQHLxnxmtAOqOqrwG9wr1Q0JubsjMDELVX9WkRewrX2uAb41Bv1IPCyiFwEfBgxyRPAqyJyNvARlZwFqGquiHwu7gXpb6vqzSLSB/jSazV7N3AhrnopUgbwbMTdQ7cf9EoaEwVrfdQYj4jcBexW1Qf9jsWYumRVQ8YYE+fsjMAYY+KcnREYY0ycs0RgjDFxzhKBMcbEOUsExhgT5ywRGGNMnPt/LpnP+BQWbDwAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -463,7 +373,7 @@ } ], "source": [ - "dY_nonlin = 100 * (td_nonlin['Y'] - 1) \n", + "dY_nonlin = 100 * td_nonlin.deviations()[\"Y\"]\n", "\n", "plt.plot(dY[:21, 2], label='linear', linestyle='-', linewidth=2.5)\n", "plt.plot(dY_nonlin[:21], label='nonlinear', linestyle='--', linewidth=2.5)\n", @@ -483,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 20, "metadata": {}, "outputs": [ { @@ -493,35 +403,31 @@ "On iteration 0\n", " max error for asset_mkt is 4.22E-06\n", " max error for fisher is 2.50E-04\n", - " max error for wnkpc is 6.15E-08\n", + " max error for wnkpc is 5.08E-08\n", "On iteration 1\n", - " max error for asset_mkt is 3.31E-06\n", - " max error for fisher is 1.58E-08\n", - " max error for wnkpc is 2.56E-06\n", + " max error for asset_mkt is 2.06E-06\n", + " max error for fisher is 1.71E-08\n", + " max error for wnkpc is 2.09E-06\n", "On iteration 2\n", - " max error for asset_mkt is 8.70E-08\n", - " max error for fisher is 2.67E-10\n", - " max error for wnkpc is 7.56E-09\n", - "On iteration 3\n", - " max error for asset_mkt is 9.90E-10\n", - " max error for fisher is 1.78E-12\n", - " max error for wnkpc is 7.87E-11\n" + " max error for asset_mkt is 6.73E-09\n", + " max error for fisher is 2.65E-10\n", + " max error for wnkpc is 7.61E-09\n" ] } ], "source": [ - "td_nonlin = sj.td_solve(ss, block_list, unknowns, targets,\n", - " rstar=ss['r']+0.1*drstar[:,2], use_saved=True)" + "td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {\"rstar\": 0.1 * drstar[:, 2]},\n", + " unknowns, targets, Js={'household': J_ha})" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEWCAYAAAB8LwAVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAAgAElEQVR4nOzdd5hU1fnA8e87sxWWuvS6dOkgRbBBYuwKqNhjSYwliqZpLLHHFEs0tsTozxZNLMFosAsqdqmCgIB0WTpLXXaX3Z15f3+cu+swzM7Ows7eLe/neeaZW86dee+de++Ze86954iqYowxxkQL+B2AMcaY2skyCGOMMTFZBmGMMSYmyyCMMcbEZBmEMcaYmCyDMMYYE5NlEPWIiNwkIv/ndxzG1EUicrGIfBoxni8i3Wvou6eLyM+q+TP3WZ8DUa8zCBE5T0Rmez/0BhF5W0SO9Duu6iAiY0UkN3Kaqv5RVat1J2toROR2EXn+IJYXEblbRPK81z0iIhWkbS8iU0RkvYioiOREzb9ORLaKyEIRGRAx/QgRee1AY6xJB7s9/aSqWaq60u84/FRvMwgR+TXwV+CPQFugC/A3YLyfcdUHIpLidwy12GXABGAwMAg4Bbi8grRh4B3gjOgZItIeuAToDjwG/NmbngL8BfhldQdeG9m+5jNVrXcvoBmQD5wZJ006LgNZ773+CqR788YCucBvgM3ABuAnEcueBHwD7AbWAdd60y8GPo36HgV6esPP4DKpt734PgPaed+9HVgCDI1YdjVwo/dd24GngQygMVCIO8Hke68OwO3A8xHLjwMWATuA6UDfqM++Fvga2Am8BGRUsK0u9mJ9ANgG3OVN/ymw2IvtXaCrN128tJu9z/4aGBCxDR4Dpnrb76Oy5bz5hwOzvOVmAYdHzJsO/N6LZTfwHtDKm5cBPA/kees7C2gbsT886f2O64C7gGCM9TwBKAZKvG0635veAZjirfty4NI4+9XnwGUR45cAX1ayv6Z4+0lOxLTDgBe84UOAb7zha4GbEjgGbgf+422T3cACoLe3P20G1gLHRaSvcB29z3oZ+Kf3WYuA4VHLvgJsAVYB11SyPX/i7Te7gZXA5RGfNRZ37F0PbASeAxYCp0akSQW2AkNirHfZ8jd5aVYD50edG/7pxboGuBkIxDp+2ffYzcRlzGtw++an3rQ3gaujYvgamBAjtnj76HQq2LcTOJY7A//11ikPeKSC9bnXi7tZwufSAzkB1/aXt2OWAilx0twJfAm0AVrjDuzfR+xkpV6aVFyGUAC08OZvAI7yhlsAh8b6QWLsZM94O+0wb2f5AHdAXQgEcSeuDyOWXe0dHJ2Blt7Oc1dEjLkxTgrPe8O9gT3Asd46/BZ34KdFfPZM3MHdEnfAXlHBtrrY2x5X405mmbh/ycuBvt60m4HPvfTHA3OA5rjMoi/QPmIb7AaOxmXSD5ZtMy+O7cAF3mee641nRxxEK7x1y/TG/+zNuxx4HWjkbcthQFNv3mvAP3AZaxtvvS+vYF3Lt2HEtI9wGXsGMAR3IB5TwfI7gcMixocDuyvZX2NlENneb98cmIQ72XcGZpf9hpV85u1AkfdbpOBOiquA33n7w6XAqkTWMeKzTvK27Z/wMj1cKcQc4FYgDXfFsxI4Ps72PBno4e0bY3DHVtkxNBa3r93t7R+ZuH33pYjlxwMLKljvsuXv95YfgzsO+njz/wn8D2gC5ADfApfEOn7Z99h9FLe/dfS2weHe558FzIhYZjDuJL3fb0T8fXQ6Fe/bFR7L3ufMx/0ha+z9fkdGro/3Gz2B+xPXqErn0uo+OdeGF3A+sLGSNCuAkyLGjwdWR+xkhURkMLh/XaO84e+8H7tp1Gfus4PF2MmeAZ6ImHc1sDhifCCwI2J8NREnbdwBuiIixngZxC3AyxHzArh/z2MjPvvHEfPvAR6rYFtdDHwXNe1tvAMr4vMLgK7AD3EH3ii8f2cR6Z4BXowYzwJCuJPfBcDMqPRfABdHHEQ3R8y7EnjHG/4pLpMfFLV8W2AvkBkx7VwiMuKKtqE33tmLr0nEtD8Bz1SwfAg4JGK8l7cPSJx9cb8MIiLOud627or7l3gMcDbuhP4/oFOc9ZgaMX4q7l980Btv4n1n88rW0fusaRHz+gGF3vBhMfaNG4GnY23PCmJ9DfhFxH5dTMTVLO5PzG6+P5lOBn5bwWeNxWUQjSOmvYw7HoLevtAvYt7lwPRYx6+3fXri9u1CYHCM70vHXXX18sbvA/5WQWwx99EE9u0Kj2VgNC4z3+/PsLc+M3ClA6+QwB+L6Fd9rYPIA1pVUn7ZAXe5WGaNN638M1S1NGK8AHcyA1dmfBKwRkQ+EpHRVYhtU8RwYYzxrH2TszZOjPHss36qGvY+q2NEmo0Rw5HrF8vaqPGuwIMiskNEduAOEgE6quoHwCO4f12bRORxEWka67NUNd9btkN0zJ41Ccb8HO4f0otepe89IpLqxZkKbIiI9R+4K4lEdAC2qeruODFFygci17UpkK/eEVsVqvqCqh6qqicCA3Ant69wJ6FTcVcV98X5iOh9a6uqhiLGwW2/RNYxertneMdXV6BD2bb1tu9NuIw5JhE5UUS+FJFtXvqTgFYRSbaoalHEdliPu3o+Q0SaAycC/4qz3ttVdU/UunTwviON/Y/7in7LMq1w/8xXRM9Q1b24DOjHIhLAZerPVfA5Fe2jZSrat+Mdy52BNVHnqkg9cVdcd6hqcdy1jKG+ZhBf4C6JJ8RJsx63c5fp4k2rlKrOUtXxuJPMa7gdBNxlYKOydCLSrgoxV6RzBTFWdsLZZ/28O2k64/55HIjo71uLK6ZpHvHKVNXPAVT1IVUdBvTHXSJfF7Fs+TqJSBauaKmsLijyNwG3zpXGrKolqnqHqvbDXf6fgiu6W4s7sbaKiLOpqvZPcD3XAy1FpEmCMS3CFTOUGexNO2Aikom72eI3uCuStaq6C1eGPehgPttT1XWMtBZXVBW5HzRR1ZO8+ftsTxFJx/2bvQ9X/t4ceAv354JYy3ieBX4MnAl8oarxYmshIo2j1mU9rni3hP2P+8rWcyvufNKjgvnP4kotjgEKVPWLWIni7KOViXcsrwW6xPkzvBhX5/O2iPRJ4Lv2US8zCFXdiSsTfVREJohIIxFJ9f653OMlewG4WURai0grL32lt+OJSJqInC8izVS1BNiFuzwHVxbYX0SGiEgG7vL6YF0lIp1EpCXun9lL3vRNQLaINKtguZeBk0XkGO9fym9wJ8rPqyEmcBXNN4pIfwARaSYiZ3rDI0TkMO979+AOrlDEsieJyJEikoarmJuhqmtxJ4re3u3JKSJyNq44443KghGRH4jIQBEJ4n6TEiCkqhtwFX5/EZGmIhIQkR4iMqaCj9oE5Hj/BvHi+hz4k4hkiMggXMVzRf9g/wn8WkQ6ikgH3HZ/Jk7cGbhiCoB0bzzazbjinvW44s0+ItIW+AGuvP+gHMA6RpoJ7BKR60UkU0SCIjJAREZ48/fZnrh/8Om4YpFSETkROC6B73kNOBT4BW4bV+YO71g9Cnci/o939fQy8AcRaSIiXYFfU8lx7/1jfwq4X0Q6eOs42svs8DKEMK4Su6Krhwr30QTWJd6xPBNXJ/pnEWns/X5HRMX/Au7cMU1EKsrkYqqXGQSAqt6P+/Fvxu2Ma3GVfWX3j9+Fq/D7GneHx1xvWiIuAFaLyC7gCtw/G1T1W1zF9jRgGa6C6GD9G3eCW+m97vK+awkuk1vpXdrvU/Skqku9uB7G/QM6FXcnSJUvM2NR1VdxFYkvetthIe7SH1yxyhO4CuY1uCK/yKKQfwO34YqWhuH+faGqebiD+TfeMr8FTlHVrQmE1A5XNr0L96/pI74/8C/EnZjK7gabDLSv4HP+473nichcb/hcXIXmeuBV4DZVnVrB8v/AVUQuwG2TN71pQPnDV0dFpC/EFUuBu4utMGIe3r++43C/I16G92fcVck1uPL+6lCVdSznnXRPxVVsr8Lta/+Hu1sIoranV4x1De6ktx04D3f3VGXfU4i78uiGq4uJZ6P32etxmdwV3vECrt5vD+5Y+hS3Lz5V2ffj7h5bgLtq24bb9yPPn//E1SHGy2zi7aMVincsR2z/nrg/D7m4Oqroz3gWd276QKKet4lHDqBo1NQQEVkN/ExVp/kdS3URkWdwles3+x2LqVtE5Fagt6r+OE6asbhK8U41Fpj73gtxtzfXiwdxy9hDKMaYWs8rYr0Ed/Veq4hII9xdR3/zO5bqVm+LmIwx9YOIXIorIn5bVT/2O55IInI8rgh7E664ql6xIiZjjDEx2RWEMcaYmOpNHUSrVq00JyfH7zCMMaZOmTNnzlZVbR1rXr3JIHJycpg9e7bfYRhjTJ0iItGtF5SzIiZjjDExJTWDEJETRGSpiCwXkRtizE8XkZe8+TPKHuAQkRwRKRSRed7rsWTGaYwxZn9JK2LyHid/FNdEbS4wS0SmqOo3EckuwTWs1VNEzsE9nVj2FOAKVR2SrPiMMcbEl8w6iJHAcvW67BORF3GtCkZmEOP5vr2iycAjXkNUxhgDQElJCbm5uRQVFVWe2FQoIyODTp06kZqaWnliTzIziI7s20R0Lq7t+JhpVLVURHbiOkoB6CYiX+HaLblZVT+J/gIRuQzXxSNdunSp3uiNMbVCbm4uTZo0IScnB/v/eGBUlby8PHJzc+nWrVvCyyWzDiLWLxn9VF5FaTYAXVR1KK7BvX/Lvv0JuISqj6vqcFUd3rp1zLu0jDF1XFFREdnZ2ZY5HAQRITs7u8pXYcnMIHLZty+DTuzf30J5Gq8982a4jkv2ei17oqpz+L4rPmNMA2SZw8E7kG2YzAxiFtBLRLp57f6fw/7N+k4BLvKGJwIfqKp6fTQEAUSkO66TlINu9z6We95Zwml/+4xbXluYjI83xpg6K2kZhNcF3iRcF3uLcX2qLhKRO0VknJfsSVynN8txRUllt8IeDXwtIvNxlddXqOq2ZMTZbOnLXL/xN1y0oNY1EmmMqSWyslzvn+vXr2fixIk+R1Nzkvoktaq+heslLHLarRHDRbguBKOXewXXOUjS9W28m1HbF0MIinblkdE0u/KFjDENUocOHZg8eXJSv6O0tJSUlNrRyEWDf5I6vfOh5cO5i7/0MRJjTG23evVqBgwYAMAzzzzD6aefzgknnECvXr347W9/W57uvffeY/To0Rx66KGceeaZ5Oe7TgPvvPNORowYwYABA7jssssoa0177Nix3HTTTYwZM4YHH3yw5lesArUjm/JRh0NGgdfF+M6Vs+Gwk/0NyBhToTteX8Q363dV++f269CU207tX+Xl5s2bx1dffUV6ejp9+vTh6quvJjMzk7vuuotp06bRuHFj7r77bu6//35uvfVWJk2axK23ukKUCy64gDfeeINTTz0VgB07dvDRRx9V63odrAafQXTsnMNmbUEb2U7Kxvl+h2OMieOb9buYsSop1ZEH5JhjjqFZM9f9dr9+/VizZg07duzgm2++4YgjjgCguLiY0aNHA/Dhhx9yzz33UFBQwLZt2+jfv395BnH22ft1Je27Bp9BBALC2oxetNk7k+zdSypfwBjjm34d9nscytfPTU9PLx8OBoOUlpaiqhx77LG88MIL+6QtKiriyiuvZPbs2XTu3Jnbb799n+cSGjdufGDBJ1GDzyAA9rQcABtm0im8juI9O0hr3NzvkIwxMRxIMVBNGzVqFFdddRXLly+nZ8+eFBQUkJubS5s2bQBo1aoV+fn5TJ48udbfEdXgK6kB0joPLR/OXTzTx0iMMXVd69ateeaZZzj33HMZNGgQo0aNYsmSJTRv3pxLL72UgQMHMmHCBEaMGOF3qJWqN31SDx8+XA+0w6DVK5aS89xIAOb2/S2Hnv276gzNGHMQFi9eTN++ff0Oo16ItS1FZI6qDo+V3oqYgM45vbgvfB4LSzvRKziGQytfxBhj6j3LIIBgMMCX7S9g9prtbN9kbb4YYwxYHUS5AR3drWqLN+yiJBT2ORpjjPGfZRCesgyiuDTMii35PkdjjDH+swzCM7BNkAdSH2Vq2nXs+fQffodjjDG+szoIT4/2bWgXmEcz2cNXG77yOxxjjPGdXUF4UlKCrEnrCUDznYt9jsYYUx9Nnz6dU045BYApU6bw5z//2eeI4rMriAg7m/eDLfPpVLKGUHEhwbRMv0MyxtRT48aNY9y4cZUnPAihUIhgMHjAy9sVRISUjkMASJUQ67+d43M0xpjaYvXq1fTt25dLL72U/v37c9xxx1FYWMi8efMYNWoUgwYN4rTTTmP79u2Aa777+uuvZ+TIkfTu3ZtPPvlkv8985plnmDRpEgAXX3wx11xzDYcffjjdu3ffp8+Je++9lxEjRjBo0CBuu+228ukTJkxg2LBh9O/fn8cff7x8elZWFrfeeiuHHXYYX3zxxUGtt11BRMjudRjMc8N5y2bSecCR/gZkjNnfV/+Cef+On6bdQDgxovhmw9fwzo2x0w45D4aeX+nXLlu2jBdeeIEnnniCs846i1deeYV77rmHhx9+mDFjxnDrrbdyxx138Ne//hVwHf/MnDmTt956izvuuINp06bF/fwNGzbw6aefsmTJEsaNG8fEiRN57733WLZsGTNnzkRVGTduHB9//DFHH300Tz31FC1btqSwsJARI0ZwxhlnkJ2dzZ49exgwYAB33nlnpetUGcsgIuT0Hki+ZpIlhYTWzfM7HGNMLDu+gzWfVm2Zop0VL5OT2B/Bbt26MWSIK2UYNmwYK1asYMeOHYwZMwaAiy66iDPP/L6DzNNPP7087erVqyv9/AkTJhAIBOjXrx+bNm0CXMdD7733HkOHuvbi8vPzWbZsGUcffTQPPfQQr776KgBr165l2bJlZGdnEwwGOeOMMxJap8pYBhEhLTWFb1N7MKB0Ic13fuN3OMaYWJp3ga6VnNTbDdx3PKNZxcs075LQ10Y37b1jx46E0pc1A16Vzy9rI09VufHGG7n88sv3STt9+nSmTZvGF198QaNGjRg7dmx50+EZGRkHVe8QyTKIKDua94WtC+lcvIpwSTGB1DS/QzLGRBp6fkJFQvtoPwh+8ma1htGsWTNatGjBJ598wlFHHcVzzz1XfjVRXY4//nhuueUWzj//fLKysli3bh2pqans3LmTFi1a0KhRI5YsWcKXXyanu2TLIKLk9xzHDRuasTCcw8Pbi+jWxjIIY0xszz77LFdccQUFBQV0796dp59+ulo//7jjjmPx4sXlPdJlZWXx/PPPc8IJJ/DYY48xaNAg+vTpw6hRo6r1e8tYc99R5q/dwfhHPwPg4XOHcurgDgf9mcaYA2fNfVefqjb3bbe5RunTrgnBgGvRdeG6nT5HY4wx/rEMIkpGapBebbIAWLjeMghjTMNldRAxnN1kAT3yXqBn7gY0tBQJ2mYyxk+qioj11XIwDqQ6wa4gYujdtJijgwvowFY2rlzgdzjGNGgZGRnk5eUd0AnOOKpKXl4eGRkZVVrO/hrH0KLHCFjkhjcvnUH7XkP9DciYBqxTp07k5uayZcsWv0Op0zIyMujUqVOVlrEMIoacvsPY+78U0qWUknXW9LcxfkpNTaVbt25+h9EgWRFTDI0yM1kdzAEga9sif4MxxhifWAZRgW1N3b3CnfYuR8Mhn6Mxxpial9QMQkROEJGlIrJcRG6IMT9dRF7y5s8QkZyo+V1EJF9Erk1mnLGE2g0GIItCtnxnHQgZYxqepGUQIhIEHgVOBPoB54pIv6hklwDbVbUn8ABwd9T8B4C3kxVjPM27f/9g4cbFM/wIwRhjfJXMK4iRwHJVXamqxcCLwPioNOOBZ73hycAx4t3sLCITgJWU309Us3L6j6BEXYuIe3OtotoY0/Ak8y6mjsDaiPFc4LCK0qhqqYjsBLJFpBC4HjgWqLB4SUQuAy4D6NIlsSZ7E5XVOIuH0i9hQX4WWXIYI6r1040xpvZL5hVErMceo590qSjNHcADqpof7wtU9XFVHa6qw1u3bn2AYVZsWbfzmBoezmeb7G5gY0zDk8wMIhfoHDHeCVhfURoRSQGaAdtwVxr3iMhq4JfATSIyKYmxxjSwY1MANu/ey+ZdRTX99cYY46tkZhCzgF4i0k1E0oBzgClRaaYAF3nDE4EP1DlKVXNUNQf4K/BHVX0kibHGNKBDs/Jha7jPGNPQJK3sxKtTmAS8CwSBp1R1kYjcCcxW1SnAk8BzIrIcd+VwTrLiORD9OzTlTylPMDiwkt2fHweH3Od3SMYYU2OSWriuqm8Bb0VNuzViuAg4M3q5qPS3JyW4BDRrlMao1OV007Us3DLPrzCMMcYX9iR1JTZnHQJAx8KlYK1JGmMaEMsgKlHSZhAALdjF9o2r/Q3GGGNqkGUQlWjSbVj58PrFX/gYiTHG1CzLICrRuf/o8uGCNXN9jMQYY2qWZRCVaNmiJd9JBwAytlrvcsaYhsMyiARsbNQHgPYFS32OxBhjao5lEAnY61VUt9Lt7Nq8tpLUxhhTP1gjQwlIO+R4bvl2J4vCOVy3PcjoNn5HZIwxyWdXEAno1m8Yz4WOY6725uuNhX6HY4wxNcIyiAS0aZJBmybpACxcv8vnaIwxpmZUmkGIyBEi0tgb/rGI3C8iXZMfWu0ysKNruG/ROmu0zxjTMCRyBfF3oEBEBgO/BdYA/0xqVLXQcVkr+b/Ue/n37ovJ37bR73CMMSbpEskgSlVVcd2DPqiqDwJNkhtW7dOrZZAfBb+inWxn7Tdf+h2OMcYkXSIZxG4RuRH4MfCmiASB1OSGVft07DuyfDh/9RwfIzHGmJqRSAZxNrAXuERVN+L6kb43qVHVQm3adWYj2QCkbvra52iMMSb5ErqCwBUtfSIivYEhwAvJDav2ERHWZbonqlvnL/E5GmOMSb5EMoiPgXQR6Qi8D/wEeCaZQdVWhdn9AeioGynYledzNMYYk1yJZBCiqgXA6cDDqnoa0D+5YdVOGV0OLR+2impjTH2XUAYhIqOB84E3vWnB5IVUe7U/ZFT58K6Vs32MxBhjki+RDOIXwI3Aq6q6SES6Ax8mN6zaqUPnbmyhOQBBq6g2xtRzlTbWp6of4+ohysZXAtckM6jaSkR4tcVPmb+plPzwIJ71OyBjjEkia821ivJ6n8WbG1YSzBOKSkJkpDbI0jZjTANgjfVV0YAOrk2mUFhZsnG3z9EYY0zyWAZRRWWN9gEstIb7jDH1WKVFTCLSDbgayIlMr6rjkhdW7dWlZSNuSX+JYbqAvTMHwqgG126hMaaBSKQO4jXgSeB1IJzccGq/QEAYnr6WwcUrWbWzwW8OY0w9lkgGUaSqDyU9kjokv2V/2DiXzqXfUVy4h7TMxn6HZIwx1S6ROogHReQ2ERktIoeWvZIeWS2W2mkIACkSZu2SWT5HY4wxyZHIFcRA4ALgh3xfxKTeeIPUps9h4D1IvX35LBg61td4jDEmGRLJIE4DuqtqcbKDqSs6d+/PLm1EUymADfP9DscYY5IikSKm+eC1L1FFInKCiCwVkeUickOM+eki8pI3f4aI5HjTR4rIPO81X0ROO5DvT5ZgMMB3aT0BaLHrG5+jMcaY5EjkCqItsEREZuE6DgIqv83V63nuUeBYIBeYJSJTVDXyjHoJsF1Ve4rIOcDduA6KFgLDVbVURNoD80XkdVUtrcrKJdOuFv1g89d0LllN6d5CUtIz/Q7JGGOqVSIZxG0H+NkjgeVe202IyIu4fq0jM4jxwO3e8GTgEREpa168TAauzqNWCXYcCptfJE1CrPp2Lt0GHuF3SMYYU60qLWJS1Y+AJUAT77XYm1aZjsDaiPFcb1rMNN7VwU5w/XqKyGEisghYAFwR6+pBRC4TkdkiMnvLli0JhFR9svuP5fclP+bsvbcwr6BNjX63McbUhEozCBE5C5gJnAmcBcwQkYkJfLbEmBZ9JVBhGlWdoar9gRHAjSKSsV9C1cdVdbiqDm/dunUCIVWfnG69eV5OYYb2Zf4mq783xtQ/iVRS/w4YoaoXqeqFuKKjWxJYLhfoHDHeCVhfURoRSQGaAdsiE6jqYmAPMCCB76wxKcEAfds3BaxNJmNM/ZRIBhFQ1c0R43kJLjcL6CUi3UQkDTgHmBKVZgpwkTc8EfhAVdVbJgVARLoCfYDVCXxnjRrQ0WUQS9dvo3jv3kpSG2NM3ZJIJfU7IvIu8II3fjbwVmULeXcgTQLexXVR+pTXI92dwGxVnYJr4+k5EVmOu3I4x1v8SOAGESnBPZx3papurcqK1YQj24UYmvp3fhSYw7ef/YUBPzzP75CMMabaiGrlNwiJyOm4k7YAH6vqq8kOrKqGDx+us2fXbD/Ru3btIPUvvciUYua0OJFhv3ixRr/fGGMOlojMUdXhsebFvYLwnmV4V1V/BPw3GcHVZU2bNmdOoxEMK/yMnts/IVRSTDA1ze+wjDGmWsStS1DVEFAgIs3ipWvISnqfDEAz8lk+612fozHGmOqTSGVzEbBARJ4UkYfKXskOrK7oddREStT1S737K7vIMsbUH4lUUr/pvUwM2a3aMj99MIOL59J163Q0HEICQb/DMsaYg1ZhBiEi76vqMUA/Vb2+BmOqc/K7nwBL5tJat7Fy/sd0H/oDv0MyxpiDFq+Iqb2IjAHGicjQyM6CGnqHQdG6HXkWYXUPhefNesXnaIwxpnrEK2K6FbgB9wT0/VHzGnSHQdE6dOrG4tQ+9C1dwt7Ny/0OxxhjqkWFGYSqTgYmi8gtqvr7GoypTlow8CYu/XIbuUVt+HDrHrq1sn6qjTF1WyKtuVrmkICho35IrrpWXd9dtNHnaIwx5uAlcpurSUDPNll0b+2uGt5ZaBmEMabuswyimogIJ/RvRwZ7abtuKpvXrfI7JGOMOSiJPAdR1uRG28j0qvpdsoKqq07pGuLq9MvJlGJmfRygzbm3+h2SMcYcsEQ6DLoa2ARM5fuH5t5Iclx1Ut8+fcmTFgA0XfWOz9EYY8zBSaSI6RdAH1Xtr6oDvdegZAdWF0kgwJo2xwDQa+837Ni0tpIljDGm9kokg1iL6yvaJKDp0NMACIiy4tP/+ByNMcYcuETqIFYC00XkTaC82zRVjeEy+QgAAB/iSURBVH54zgB9R/yQLe82pzU7SF/+FvBrv0MyxpgDksgVxHe4+oc0oEnEy8SQkpLC8pZjAOhTMJc9O/N8jsgYYw5MpVcQqnoHgIg0caOan/So6riMgRPgo/+RKiEWffoKQ06+zO+QjDGmyhK5i2mAiHwFLAQWicgcEemf/NDqrr6jT2Knek1tLH7d32CMMeYAJVLE9Djwa1Xtqqpdgd8ATyQ3rLotIyODxc2OZEb4EKbs7sPe0pDfIRljTJUlUkndWFU/LBtR1ekiYi3RVWLLD+/n6hfnA3DUijx+0KeNzxEZY0zVJHIFsVJEbhGRHO91M2DtSFTiB33bkRZ0m/c9a7zPGFMHJZJB/BRoDfwXeNUb/kkyg6oPstJTOLJXKwDeW7SJUFh9jsgYY6omkbuYtgPX1EAs9c7JhzSj+beTOb54Nt/OKKHv6JP8DskYYxIWr0/qv6rqL0XkdVwPcvtQ1XFJjawe+EHPZoxLfYJUCTFr7n/AMghjTB0S7wriOe/9vpoIpD5q2aot8zMGM3jvXLpt+RANh5BA0O+wjDEmIRXWQajqHG9wiKp+FPkChtRMeHXfnm4nAtCK7ayc95HP0RhjTOISqaS+KMa0i6s5jnqr+1FnEVYBIG/2Kz5HY4wxiYtXB3EucB7QTUSmRMxqAlgDQwlq1zGHxamH0Ld0MR03TANVEPE7LGOMqVS8OojPgQ1AK+AvEdN3A18nM6j6ZluX42HlYjrqRtYunU3nQ0b4HZIxxlQqXh3EGlWdrqqjo+og5qpqaSIfLiIniMhSEVkuIjfEmJ8uIi9582eISI43/VivzacF3vsPD3QFa4NOh59VPrzhS+sjwhhTNyTSWN8oEZklIvkiUiwiIRHZlcByQeBR4ESgH3CuiPSLSnYJsF1VewIPAHd707cCp6rqQFwdyHPUYV179md5oBsA2Wun+RyNMcYkJpFK6keAc4FlQCbwM+DhBJYbCSxX1ZWqWgy8CIyPSjMeeNYbngwcIyKiql+p6npv+iIgQ0TSE/jOWmtxj0v4bcmlnLnnOtbvKPQ7HGOMqVQiGQSquhwIqmpIVZ8GfpDAYh1x3ZWWyfWmxUzjFVvtBLKj0pwBfKWqe6OmIyKXichsEZm9ZcuWRFbFNzljLuTl0A/YRlNrm8kYUyckkkEUiEgaME9E7hGRXwGJtOYa61ad6Cey46bx+p24G7g81heo6uOqOlxVh7du3TqBkPwzoGNTOjbPBODdRZt8jsYYYyqXSAZxARAEJgF7gM64f/WVyfXSlukErK8ojYikAM2Abd54J1zjgBeq6ooEvq9WExGO698WgCWr1rBt+zafIzLGmPgqzSC8u5kKVXWXqt6hqr/2ipwqMwvoJSLdvCuQc4ApUWmm8P2DeBOBD1RVRaQ58CZwo6p+lvjq1G6n9Ejj+dQ/MCvtClZ98JTf4RhjTFwVZhAi8rL3vkBEvo5+VfbBXp3CJOBdYDHwsqouEpE7RaSsob8ngWwRWQ78Gii7FXYS0BO4RUTmea863+POkD7d6RNcT4qESV/2lt/hGGNMXKIau58CEWmvqhtEpGus+aq6JqmRVdHw4cN19uzZfodRqS8eupjR216lRIPs/dW3ZDVv5XdIxpgGTETmqOrwWPPiPSi3wRs8HSj1iprKX8kItCHIHOzu9E2VEMs+nexzNMYYU7FEKqmbAu+JyCcicpWItE12UPVZ31EnslPdTWCy5A2fozHGmIolUkl9h6r2B64COgAfiYg9DnyA0tMzWNLsSAD67J5BUcFunyMyxpjYEnpQzrMZ2IhrybXOVxj7KdjvVAAypZhvP3/N52iMMSa2RNpi+rmITAfex7XseqmqDkp2YPVZ3yPHU6Cu5ZCSha/7HI0xxsQWr7nvMl2BX6rqvGQH01A0zmrKnMYjGLBnBht2FlIaCpMSrMrFnDHGJF8idRA3AFki8hMAEWktIt2SHlk9t2H0bRy69zEmFV7B1G+s6Q1jTO2TSBHTbcD1wI3epFTg+WQG1RCMGTGU1EZNAbh/6reEwrGfRzHGGL8kUq5xGjAO1w4TXjPcTZIZVEPQJCOVn4/pAcCyzflMmZfrc0TGGLOvRDKIYnWPWyuAiCTSkqtJwIWjc+icpfw8OIUBr59Cyd4Cv0MyxphyiWQQL4vIP4DmInIpMA14IrlhNQyZaUH+fMgKrk99kV66mvmvPeB3SMYYUy6RSur7cL29vQL0AW5V1UR6lDMJGDHu56yV9gB0X/wPivZU2purMcbUiER7lJuqqtep6rWqOjXZQTUkaWlprBvyKwBaspMF/727kiWMMaZmxGvue7eI7KroVZNB1nfDT76ElQHXaG6fFU+TvzPP54iMMSZ+a65NVLUp8FdcPw0dcb3CXQ/cVTPhNQwpKSlsG/lbAJqyh28m2+Y1xvgvkSKm41X1b6q62+tV7u8k1uWoqYJDjz2PpcHeAAz47l/s3LLO54iMMQ1dIhlESETOF5GgiARE5HwglOzAGppAMEDhUe5ZxEayl6Wv/N7niIwxDV0iGcR5wFnAJu91pjfNVLPBR09gYepA1oTb8ML61mzeXeR3SMaYBqzSxvpUdTUwPvmhGAkE2Dv+CSY8/y2lpND0g+XcMX6A32EZYxooa0K0lhk2oC+je7UD4N8zvyN3uz1dbYzxh2UQtdC1x/UBoCSkPPvOZz5HY4xpqCyDqIUGd27Oub2Uh1Mf4volZ/Hd0rl+h2SMaYASziBEZJSIfCAin4nIhGQGZeCy0e05OTCDFAmT9/ptfodjjGmA4j1J3S5q0q9xzX6fANg9mEnWrd8w5jQ7FoCh+R+z4utPfY7IGNPQxLuCeExEbhGRDG98B+721rMBa2qjBrQffwclGgRgz9t3+ByNMaahidfUxgRgHvCGiFwA/BIIA40AK2KqAZ169GNu9ikADCqcyZKZ1k6iMabmxK2DUNXXgeOB5sB/gaWq+pCqbqmJ4AzknH4bezUVgPC0O0Gta1JjTM2IVwcxTkQ+BT4AFgLnAKeJyAsi0qOmAmzo2nbqwdy2EwHoV/w1Cz/9n88RGWMainhXEHfhrh7OAO5W1R2q+mvgVuAPNRGccXpPvIU96qqCUj/6IxoO+xyRMaYhiJdB7MRdNZwDbC6bqKrLVPWcZAdmvpfdpiNfdzqfqaFD+eWei5m2xEr4jDHJFy+DOA1XIV3KATbOJyIniMhSEVkuIjfEmJ8uIi9582eISI43PVtEPhSRfBF55EC+u77pd/6f+E3wBhZrV/7y3lLCYauLMMYkV7y7mLaq6sOq+piqVvm2VhEJAo8CJwL9gHNFpF9UskuA7araE3gAKOtvswi4Bbi2qt9bXzVrlM7lY1zVz5KNu3n96/U+R2SMqe+S2dTGSGC5qq5U1WLgRfZvFXY88Kw3PBk4RkREVfeo6qe4jMJ4fnJEDq2y0ggQZu7bT1NSUux3SMaYeiyZGURHYG3EeK43LWYaVS3F1XtkJ/oFInKZiMwWkdlbttT/cvlGaSncODKVd9Ku54699/LVlL/5HZIxph5LZgYhMaZFF5wnkqZCqvq4qg5X1eGtW7euUnB11SlHj6BZwF1YdVnwMHuLrDlwY0xyJDODyAU6R4x3AqILzsvTiEgK0AzYlsSY6rz0jMZ8N3ASAO3YytynfuVzRMaY+iqZGcQsoJeIdBORNNztslOi0kwBLvKGJwIfqNqjwpUZeupVrArmADB684t8+fJ9/gZkjKmXkpZBeHUKk4B3gcXAy6q6SETuFJFxXrIngWwRWY5rLbb8VlgRWQ3cD1wsIrkx7oBqsFLS0sm88CW20RSA4Yv+wLyPXvU5KmNMfSP15Q/78OHDdfbs2X6HUaOWz3mfzlPOJl1K2KWN2HL26/ToN9zvsIwxdYiIzFHVmCcO61GuDus57BgWj7oHgKZSgPznYjbt2ONzVMaY+sIyiDpuyIk/ZVa3K9mizfhV0aVc8txcCopL/Q7LGFMPWAZRDwy/4A/8o//zzNeeLFy3i1+8OI+QNcVhjDlIlkHUAxIIcP0ZR3JUr1YATP1mEw/+7zOfozLG1HWWQdQTqcEAj55/KL3bZvGDwFdcNu8MPnvlYb/DMsbUYZZB1CNNM1J56tx+/CXtH2RJESO+vo2vPnnD77CMMXWUZRD1TKd2rck76QmKNUiahOg27XJWLJnvd1jGmDrIMoh6qNfIE1g8wnX611zySX3pbLZs2uBzVMaYusYyiHpq8Ck/Z07XnwHQRTew8YkzKSi0hv2MMYmzDKIeO/Sie5jf7IcADCxdwLxHLyIUsv6sjTGJsQyiHpNAkH4/f55laX0BODz/Pab+8w8+R2WMqSssg6jnUjMa0/ay/7JB2vB5qB+/Xdqb579c43dYxpg6wDKIBqBpqw7oRW/w67Rb2EUWt01ZxEff1v8e+IwxB8cyiAaiQ04f/n7RaNJTAoTCylX/msuCFd/5HZYxphazDKIBGdqlBQ+cPQQhzNWhZ8n550jef+lhwtZukzEmBssgGpiTBrbn6ZEbuDzlTZpIIccsvpnP7z2NDRs3+h2aMaaWsQyiARp7+mWs+NGTbKcZAEcWfog+dgSfvh/dI6wxpiGzDKKB6nHkRNKu+ZIlWaMA6MBWRn98IVMfvopde+yBOmOMZRANWuOWHTjkN++weOgtFJFKUJRj854n976jmD9/jt/hGWN8ZhlEQydC3/HXsufCaXyX2h2A3uGV3P7Sp9z77hJK7MlrYxosyyAMANndh9Dpui9Y2PVCHg5P5KtwTx79cAVn/P1zVmzJ9zs8Y4wPLIMw5QJpGQz4ycOcPOl+DmnXBICvc3dy70MP8f6bL6Jqt8Ma05BYBmH207ttE/436QguO7o7bdnGnwKPcsysy5n6wM/YumOX3+EZY2qIZRAmpvSUIDed1JenT8wkXUoBOG7XZLb99UhmzLD+ro1pCCyDMHH1GzOR0p9NZ3X6IQD0Zg2D3xrPWw9NYv43S6zYyZh6zDIIU6mmnfrS9bpPWNzrMkIqZEgJJ217jr4vHc5HfxzP2x98SGFxyO8wjTHVzDIIkxBJSaPv+feydeJ/WZveG4A0CTG25COenjqHUX96nz++tZjv8uwhO2PqC6kvRQTDhw/X2bNn+x1Gw6DK9m8/Y/O0hwltXcZJhXcCAoCIck+76XQ++gJGDh5EICD+xmqMiUtE5qjq8JjzLIMwB6OkpJipS/J49vPVzFi1jcMDC/l32h8JqfBZymHkD76EI4+dQNPMNL9DNcbEYBmEqRFLNu5i439vYuzm5/aZvkw78U2nc+h34qX06tTOp+iMMbHEyyCSWgchIieIyFIRWS4iN8SYny4iL3nzZ4hITsS8G73pS0Xk+GTGaarHIe2aMvbKR9h98Qcs7TCBvbirhl6Sy/h199H2iaG8de/FvP3x56zbUehztMaYyiTtCkJEgsC3wLFALjALOFdVv4lIcyUwSFWvEJFzgNNU9WwR6Qe8AIwEOgDTgN6qWuGtMnYFUfuE8/NYNfXvNFv4T1qFNpVPv7nkJzwfOpZOLTIZ1T2bozoGOLRPNzpnZ/kYrTENk19XECOB5aq6UlWLgReB8VFpxgPPesOTgWNERLzpL6rqXlVdBSz3Ps/UIYGsbHqcdjOtfreYzSc9yaqmbh/8MtwXgNzthUyek0uzt6+iyUO9+ejO43j17zfz7gdT+W5rvj1jYYzPUpL42R2BtRHjucBhFaVR1VIR2Qlke9O/jFq2Y/QXiMhlwGUAXbp0qbbATTULBGkzciKMnEjplhXcV9iSL1dtY8bKPL5avYXhLCVLihgTngGbZsCmh9nxUWM+CfYnr/VIGvUawyFDRtMlOwv3/8EYUxOSmUHEOpKj/xJWlCaRZVHVx4HHwRUxVTVAU/NSWvdgCDCkSwuuGNOD0r0FbPzkdjYs+4hWW2fSIpQHQHPZw9HhmbBpJmx6hMs/+BXzs47i0K7N6daqMf0yt9OuQxc6t82mdVa6ZRzGJEEyM4hcoHPEeCdgfQVpckUkBWgGbEtwWVMPpKQ3otOPfg4/+jmoEspbyYZ5Uyla/hEtt8ykZWgrADPDfdi+q4i3Fri+sz9Nv4ZOspWN2oJZtGd7eicKmnRFWnYns11vsjsfQpd2rWjdxDIPYw5UMiupU3CV1McA63CV1Oep6qKINFcBAyMqqU9X1bNEpD/wb76vpH4f6GWV1A2Ml2Gs++YLpgaO4MuVeSzduJvN23fyTdrFBCT+vvuz4t/wecpIumY3plurRhyWupKW6SHSm7Qis3kbslq0oUWTJrRonEbTjBTLSEyDFK+SOmlXEF6dwiTgXSAIPKWqi0TkTmC2qk4BngSeE5HluCuHc7xlF4nIy8A3QClwVbzMwdRTIgRb9aDL0T24BLjkyG4AFBcVsGXOIxRsWEpo60rSd62iWVEuTcM791l8nbaioDjE4g27WLxhF5ek3c2wwLJ90uzWTHZoFmtowu5gU97NOJklzY+iZaM0WjROo5espW0wn7QmrUjLzCI1ozFpGY1Ja9SYzPQMMlKDZKYFyUwNkpEaJGhPjvtKVQkrhMJKWJVQWAmFw4RDITcsAcJhCKkSLi5Ei3YTDpeioVLCoVJC4VIIhQmHSwmHQ4QIUtCkOyFVVEGLC2iU9zWEQoTDIQiXouEwGg6h4VLvPcT6NmMpDqSXx9F13ZsESwsgHAIN7fOuGkI0zLfNj2ZTZnfUi79f3ru027MUvPmEwwgh965u+NuMwXzR9HjCYbhjfH/aNs2o1u2ZzCImVPUt4K2oabdGDBcBZ1aw7B+APyQzPlM3pWU0ou0RP95/RuF2SreuZNvaxeRv+JbzWo9lxQ5l1dY9rMnbQ7f8jfst0kQKaSKFdGYLKLyxaxgzt28rn39XypOcnPJ+zDhKNEghabweOpzflV7iYksJMDb1Gy6WNyiVdEqDGYSC6YQlFQ2koBJEJcielOZMzz6HYEBICQiNtIBRO9+EQAoSCEIgxQ0HUxBxNxsqworssZQGMxEBQeiRN52UcBHA91dAXvqymrxNTQayK72dO8EB7Xd8RdOiXFB3ogEFVSCMhhVQtmbk8F3TYYS9E27r/KX02Pkl4C2jYe8kV3ayCpMfaMLUlue5ZcJK45I8Tt32DAENIerSBjQEqgghAt6yDzb5FbvIKj+Z3pz/B5rpTgIaJoBbJuClD+Be93EBH+pwd9JX5Q55ghMCMwh684OECRIilTBB70rz36U/4KbSS8t/v/OC7/PH1Cfj7msrw+04tfj+8vHusp4P0q+NuwzAyKJH2UyL8vEZ6ffQVnbEXeY/ywP8L6KH3wdT3+OI4Odxl1mRt5e31gwG4Nrj+9C2aaWhVUlSMwhjalRmC1I6D6NN52G0AbpHzlNFN71D4a6t7Nm+maKdmynevYXQnjwo2EawaBtpe7fTvn1vRtKS7XuK2V5QTMviirtbTZUQqRSSwvcXt8WlYZrrBg5PnesmVHDduybchuvWjykf7yybuCv9kUpXcdSCh9lIdvn4F+l/pL1si7ME/LL4Sl4LH1k+/kDqMxwfjN+nx4ulY/lH6fcnuHOCn3JF6v/FXWZ1uC3XrB1bPp4jG7gv/c24ywCsWLeZjREbqmf6UtrJ9rjLBIvzyQ+Xlo9npBbRQuJ3jRuMus8llMBd/kH27ZM9kWViLRdOYLnUAKQFAgQEgiKEA2kUkk7Iyxa9bJUwQVSEMAEkswU9M7MICKQGq//q1TII0zCIIO0GkNkOMuMku8Z7lQlv7UH+llUU7NxCadEeSvfuIbS3kHBxAeHiArSkkPZN+nNj9iEUloQoLAnRbfMqvtt4CCmhIlLDRaSG9xLU0vJ/wUFCSEoaPZu7f82l4TDtS1KhJDmrHog6WWnMmwT3lRKA9JQAwYAQECFVYp8qSgmgZSewYBrdWzVGBAIitKc523a3ICyB8v/1WjYsAZQgYQkwomNr8lOyy78rd9NgdoZ3EvautlQCqAQhYnhQmwG0bNKNYAACASFt6zF8ld8BDQRAghAIuiupQIo3HKRV00P4fbsBBEUIBqBZfmPmbWvjrtgkiARTyq/gJOCW1dTG/KvTYQRECAaEYKiIZVvbIoEggUAQCaQgQW84mEIgECQQTOE/LboTTE0jKEIgIAT3fMQuIJDi5gcCKQSCQYJB9y4S5L5ACvcFIjOSEyr9nc71XslibTEZUxuEQ1Cc797DpRHvJV7xj6d5F1dU5RUXsW0VGipBvX/H7nhWr6gIQKFpBySjuVcsBVKwlUDJHld0Vf4Sr2jKe09Jh4yI8oqQF4uUnXiDbhlT5/lSSW2MqYJAEDKaJZRUiDg3t+oeL2lsTdpUfZlginuZBsU6DDLGGBOTZRDGGGNisgzCGGNMTJZBGGOMickyCGOMMTFZBmGMMSYmyyCMMcbEVG8elBORLcCag/iIVsDWagqnOllcVWNxVY3FVTX1Ma6uqto61ox6k0EcLBGZXdHThH6yuKrG4qoai6tqGlpcVsRkjDEmJssgjDHGxGQZxPce9zuAClhcVWNxVY3FVTUNKi6rgzDGGBOTXUEYY4yJyTIIY4wxMTWoDEJEThCRpSKyXERuiDE/XURe8ubPEJGcGoips4h8KCKLRWSRiPwiRpqxIrJTROZ5r1tjfVaS4lstIgu8792vRyZxHvK22dcicmiS4+kTsR3micguEfllVJoa214i8pSIbBaRhRHTWorIVBFZ5r23qGDZi7w0y0TkohqI614RWeL9Tq+KSPMKlo37mychrttFZF3E73VSBcvGPX6TENdLETGtFpF5FSybzO0V8/xQY/uYqjaIFxAEVuC6Kk4D5gP9otJcCTzmDZ8DvFQDcbUHDvWGmwDfxohrLPCGT9ttNdAqzvyTgLdx/diMAmbU8G+6Efegjy/bCzgaOBRYGDHtHuAGb/gG4O4Yy7UEVnrvLbzhFkmO6zggxRu+O1ZcifzmSYjrduDaBH7ruMdvdccVNf8vwK0+bK+Y54ea2sca0hXESGC5qq5U1WLgRWB8VJrxwLPe8GTgGJHk9quoqhtUda43vBtYDHRM5ndWs/HAP9X5EmguIu1r6LuPAVao6sE8QX9QVPVjYFvU5Mj96FlgQoxFjwemquo2Vd0OTCWRTogPIi5VfU9VS73RL4FO1fV9BxNXghI5fpMSl3cOOAt4obq+L1Fxzg81so81pAyiI7A2YjyX/U/E5Wm8A2knkF0j0QFekdZQYEaM2aNFZL6IvC0i/WsqJlzXx++JyBwRuSzG/ES2a7KcQ8UHrV/bC6Ctqm4Ad4ADsfr49HO7AfwUd+UXS2W/eTJM8oq+nqqguMTP7XUUsElVl1Uwv0a2V9T5oUb2sYaUQcS6Eoi+xzeRNEkhIlnAK8AvVXVX1Oy5uGKUwcDDwGs1EZPnCFU9FDgRuEpEjo6a78s2E5E0YBzwnxiz/dxeifJzX/sdUAr8q4Iklf3m1e3vQA9gCLABV5wTzbftBZxL/KuHpG+vSs4PFS4WY1qVtllDyiBygc4R452A9RWlEZEUoBkHdjlcJSKSivvx/6Wq/42er6q7VDXfG34LSBWRVsmOy/u+9d77ZuBV3KV+pES2azKcCMxV1U3RM/zcXp5NZcVs3vvmGGl82W5eReUpwPnqFVRHS+A3r1aquklVQ6oaBp6o4Pv82l4pwOnASxWlSfb2quD8UCP7WEPKIGYBvUSkm/fv8xxgSlSaKUBZTf9E4IOKDqLq4pVvPgksVtX7K0jTrqwuRERG4n63vGTG5X1XYxFpUjaMq+RcGJVsCnChOKOAnWWXvklW4b86v7ZXhMj96CLgfzHSvAscJyItvCKV47xpSSMiJwDXA+NUtaCCNIn85tUdV2Sd1WkVfF8ix28y/AhYoqq5sWYme3vFOT/UzD6WjJr32vrC3XHzLe5uiN950+7EHTAAGbgii+XATKB7DcR0JO6y72tgnvc6CbgCuMJLMwlYhLtz40vg8BraXt2975zvfX/ZNouMTYBHvW26ABheA3E1wp3wm0VM82V74TKpDUAJ7h/bJbh6q/eBZd57Sy/tcOD/Ipb9qbevLQd+UgNxLceVSZftZ2V37HUA3or3myc5rue8fedr3ImvfXRc3vh+x28y4/KmP1O2X0WkrcntVdH5oUb2MWtqwxhjTEwNqYjJGGNMFVgGYYwxJibLIIwxxsRkGYQxxpiYLIMwxhgTk2UQxtQQEWkuIlf6HYcxibIMwpgaICJBoDmuxeCqLCciYsep8YXteMbEICK/8/oemCYiL4jItSIyXUSGe/NbichqbzhHRD4Rkbne63Bv+livLf9/4x4E+zPQw+s34F4vzXUiMstrqO6OiM9bLCJ/w7Ur1VlEnhGRheL6HfhVzW8R0xCl+B2AMbWNiAzDNeUwFHeMzAXmxFlkM3CsqhaJSC/cU7nDvXkjgQGqusprjXOAqg7xvuc4oJeXRoApXkNv3wF9cE++XunF01FVB3jLxezox5jqZhmEMfs7CnhVvfaKRKSyNn9SgUdEZAgQAnpHzJupqqsqWO447/WVN56FyzC+A9ao618DXEcv3UXkYeBN4L0qro8xB8QyCGNii9UGTSnfF8tmREz/FbAJGOzNL4qYtyfOdwjwJ1X9xz4T3ZVG+XKqul1EBuM6gLkK13nNTxNZCWMOhtVBGLO/j4HTRCTTa6nzVG/6amCYNzwxIn0zYIO65qovwHWPGctuXLeRZd4Ffuq19Y+IdBSR/Tp+8ZoqD6jqK8AtuK4xjUk6u4IwJoqqzhWRl3AtZ64BPvFm3Qe8LCIXAB9ELPI34BURORP4kAquGlQ1T0Q+E5GFwNuqep2I9AW+8Fonzwd+jCumitQReDribqYbD3oljUmAteZqTCVE5HYgX1Xv8zsWY2qSFTEZY4yJya4gjDHGxGRXEMYY8//t1YEAAAAAgCB/6wVGKIlYggBgCQKAJQgAliAAWAHUJ9sq43hvHgAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] @@ -533,7 +439,7 @@ } ], "source": [ - "dY_nonlin = 100 * (td_nonlin['Y'] - 1) \n", + "dY_nonlin = 100 * td_nonlin.deviations()[\"Y\"]\n", "\n", "plt.plot(0.1*dY[:21, 2], label='linear', linestyle='-', linewidth=2.5)\n", "plt.plot(dY_nonlin[:21], label='nonlinear', linestyle='--', linewidth=2.5)\n", From f5348474a65258b2a20455e1996d9507858ec781 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 6 May 2021 14:42:42 -0500 Subject: [PATCH 130/288] Fix remaining issues with two_asset. - finance block: pshare(-1) - dividends: psip was not subtracted from dividends. - hetoutput takes ra, not r --- notebooks/two_asset.ipynb | 68 ++++++++++++++++----------- sequence_jacobian/models/two_asset.py | 8 ++-- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/notebooks/two_asset.ipynb b/notebooks/two_asset.ipynb index 18543f5..295310c 100644 --- a/notebooks/two_asset.ipynb +++ b/notebooks/two_asset.ipynb @@ -73,7 +73,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -86,7 +86,7 @@ "helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2]\n", "\n", "calibration = {\"Y\": 1., \"r\": 0.0125, \"rstar\": 0.0125, \"tot_wealth\": 14, \"delta\": 0.02, \"kappap\": 0.1, \"muw\": 1.1,\n", - " \"Bh\": 1.04, \"Bg\": 2.8, \"G\": 0.2, \"eis\": 0.5, \"frisch\": 1, \"chi0\": 0.25, \"chi2\": 2,\n", + " \"Bh\": 1.04, \"Bg\": 2.8, \"G\": 0.2, \"eis\": 0.5, \"frisch\": 1, \"chi0\": 0.25, \"chi2\": 2, 'psip': 0.0,\n", " \"epsI\": 4, \"omega\": 0.005, \"kappaw\": 0.1, \"phi\": 1.5, \"nZ\": 3, \"nB\": 50, \"nA\": 70,\n", " \"nK\": 50, \"bmax\": 50, \"amax\": 4000, \"kmax\": 1, \"rho_z\": 0.966, \"sigma_z\": 0.92}\n", "unknowns_ss = {\"beta\": 0.976, \"chi1\": 6.5, \"vphi\": 1.71, \"Z\": 0.4678, \"alpha\": 0.3299, \"mup\": 1.015, 'w': 0.66}\n", @@ -118,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 3, "metadata": { "scrolled": false }, @@ -152,7 +152,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "metadata": { "scrolled": true }, @@ -206,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -271,14 +271,14 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 7, "metadata": { "scrolled": true }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbMAAAEYCAYAAADWNhiqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdd3gc5bX48e9ZrXq1ZEm2ZUtykW3ZgG2wTTPNphguCZBAwDE1IYQAAUIKDjeBdMy9JKGHtEtCfsH0EloCGIzBNJvu3uWKLav3tuf3x6zslbTSrnZXWsk+n+fZR7uzM++cnS1HM3PmfUVVMcYYYwYzV7QDMMYYY8JlycwYY8ygZ8nMGGPMoGfJzBhjzKBnycwYY8ygZ8nMGGPMoGfJzBhjzKBnycwYY8ygZ8nMRJSIbBWRU6Mdx6Go87YXkVUicnI01j1Q2+xhXT1uq4H0uRaR20XkxmjH0R9E5AMRmRzMvEEnMxG5XEQ+F5F6EflCRP4gIhm9WD5iH4aB9MEaCELdHiISLyJ/FZESEakRkY9F5MxO8ywRkUYRqfXe1kUu8sFhsH7eVHWyqi6JdhyDQedtNVDfcxHJBi4F/ugzLVNEnhGROu93+evdLBvw+x7BOIOKyTtvoYi8JCIV3txyn4i4vU/fCfwimHUGlcxE5PvAHcAPgXTgGKAAeFVE4oJpI5p8NozpyA1sB07CeV9/CjwuIoWd5rtOVVO8twn9G+LgZp89E2GXAy+paoPPtPuBZiAXmA/8oZu9mWC/75EQbEwADwB7geHAVG9813if+xdwiogMD7hGVe3xBqQBtcDXOk1P8QbwDe9jBcb5PP834Ffe+/8APECDt60fAVuBHwOrgQrgISDBZ3m/7flrq5u4twI3A58BTThv5AjgKaAU2AJc7zP/zcBOoAZYB8zp1JbfWIFiYAlQCawCvuwnjh9446gCHvNZ1u86e4rTz+v0t217jCnA+/0Z8FWfx0uAK3uxfE/bqsf3vJu2fuiNqQ74K86X42XvNnsNGBKB96Gnz4W/7bsA2OSNYTVwXoDP3g+BpzrNcy9wVwjbsNvX6V3uVH+PgVHA097XWAbc553e29i6+8x2u32DiLu72Hzjn+h9by4K9H31afcK4HmfxxuBx30ebwem+llXd79X3b6+brZVKvAn73u4F/hesN+jHtp8HbjY53EyTtIY3+kzuzCU73skbr2NCVgDnOXz+H+BP/o8fhW4LOB6gwhsLtAKuP0893dgkfd+t8mshy/aSu8HORNY1mn+npJjh7a6iXsr8Im3/UScvdAPgVuBOGAMsBk4A5jg/WCP8C5bCIwNFCsQ6/2C3OJtczbOl2tCp2U/wPnBzPS+cVd3t86e4gzwWtu/iAFj6qGdXKARmOgzbQnOj8w+7+s+OYjt7vd9DfSed9PWe9648nB+ED4CpgHxOF/s28J8HwJub7p+di/wtuMCLsRJtMN7+OwN986T4X3e7X0tR/VmGwZ6nX7i3AqcCsQAnwK/x/mhSQBmeecJOjZ6+J50t30DfSYDxNYe/5HANuDsQHF0incMTvJ0eV9nCbDT57kKwNXTtgv0+QnwXViMk3Tjva+5DRjWaZ4XvDH6u73gp81SYIbP42lAQ6d5foBPEu/N9z0St97GhPM9fBhIwvmer8TnH0TgHuB3gdYbzGHGocA+VW3189xu7/Ohuk9Vt6tqOfBrYF4Ybflzj7f9BmAGkK2qv1DVZlXdDPwZuAjnQxYPTBKRWFXdqqqbgoj1GJw91IXeNl/H+XB2fh33qOou77LP4+xKd7fOnuIMRrAxdSAiscA/gb+r6lqfp27G+eLn4fyX+byIjA0QQ0/va2/f83tVdY+q7gTeAt5X1Y9VtQl4BueLE8770OvtrapPeNvxqOpjwAZgpp91bVfVBlXdDSzFSYLg/IO4T1U/7OF1h/N562wmzo/wD1W1TlUbVfVt72vpTWyBvif+ti8B4u42Nq8TcA41XaaqLwQZB97XthknabYfuvoPsFNEJnofv6WqngDbzld3r68LETnbG8Mdqtrkfc07gfGdYjxbVTO6uZ3tp+kM72tql4Kzp+irCmevsFs9fN8jobcxvQlMBqqBHcAK4Fmf52twXnePgklm+4Ch3Rz7H+59PlTbfe6X4HyoI8m3/QJghIhUtt9w/lPMVdWNwI3Az4C9IvKoiHSOxV+sI4Dtnb4QJTg//L6+8LlfD6T0sM5u4wzyNQcb034i4sI5DNAMXOf7nKq+r6o13i/k33H2Es4KEENP72tv3/M9Pvcb/DxOIYz3gRC2t4hcKiKf+Mx/GF3/qdve6fHfgYu99y/G2d49Cefz1tkooKSbf0iDji2I74m/7UuAuAPFdjXwjqq+0Ys4fL0JnAyc6L2/BCeRneR93BvdvT5/vgw81/7A+x1Lp+PnNxQVdEwKtTingnyl0THhddDT9z1Cgo7JG8t/cA4zJ+N8j4bg1Gi0S8XZU+1RMMnsXZzj/l/pFEQycCbOrjQ4b26SzyzDOrWjftoe5XM/H9jl87in9vy15Y/vfNuBLZ3+80lV1bMAVPURVZ2F8+OmdNyY3cW6CxjlfUN8n9sZVHD+19ljnEG8zl7FJCLCgXNRX1XVlkBhAxJgnp7e156eC1U470Mw23v/9hWRApw9t+uALFXNwDks0nmbdP6MPgscISKHAWfj/Ffck0h+3rYD+T0UowQdWxDfE396ijtQbFd7n/99iHG0J7MTvPffJLhkFuxvTHeOxjn/1242zh5vh2pgEXnZp1K48+1lP+1+Rse9u/WAW0SKfKZNwTkv2UUI3/dQ9CamTJzP+n3ef5jLcM4R+37/inEORfcoYDJT1Srg58C9IjJXRGK91S9P4OwStv8X9wnwdRGJEZG5OB8WX3twDlf5ulZERopIJs5/w4/5PNdTe/7aCuQDoFpEbhaRRG+7h4nIDBGZICKzRSQe5xhyA86hjECxvo9zvuFH3u1yMvAl4NFAwfSwzm7j7KE53+3R25j+gPNh+ZJ2rJBCRDJE5AwRSRARt4jMx/kP9z8BXl5P72tPz4Uq5PeB4La37/ZNxvmhKwUQkStw9sx6pKqNwJPAI8AHqrotwCKR/Lx9gHNKYKGIJHvfz+N7G1uQ3xN/eoq7x9hw/pufC5woIgtDiONN4BQgUVV34ByqngtkAR/3EHMovzF444sFioDzva9nMk7F3s2d51XVM/VApXDnm7+y+Zfw+S1U1TqcvZpfeLff8cA5dL/n3+33PVJ6E5Oq7sMp7PmO9zcmA7gMb/LyvsdH4RSBBFxxsCf1vonzH2j7oZ4/4q0k8z4/HSfz1niDXkTHgo5zcE7iVuKcDNzKgYqtSpxDHUnBtNe5rW7i3UqnIhGcwx2LcA4XVOAUF5wKHIHzpaoBynGO54/o1JbfWHGO9b6Jc0y4u8o23xPJPwP+X0/r7C7OHt6bztu2x5h8lmv/r7YR59BA+22+9/lsYLk3xkpvHKcF+Jz0tK16fM8DvYfe7fYzn8dXAq+F8z4Es739bN9fe9+zfcDvvOu9srt1+Uyf5d3eV4SxDbt9nX5e4/7HOHtCz+LsLezDOf/Tq9jo+TPb7fYNIm6/sXWKPxPnB+6XPcXRTdy7gYd8Hq8AXg7w+fD3e9Xt6/OznVZ7Y63COad6WU/vebA3nMNwO3CSc/u0TO/2q/PG/HWf514Gbgny+75/3k7r9Ds9wDJBxeR9PBXn8G+F9/1/AsjxPncB8HQw20a8C/Q7EdmK8wPwWlQC6IXBFOtAdqhvRxHJB9biVLRV9zDfVvp5OwUbmwlMRC7GSdZf7aP2fwPsVdW7+qL9gURE3ge+qaorA81rF3Qa0w+854tuAh4daMliIMc2SE3BKd3vE6p6S1+1PdCo6tHBzmvJzJg+5i2W2oNTwTc3yuF0MJBjG8SOIHC1qomwqB1mNMYYYyLFes03xhgz6B1UhxmHDh2qhYWF0Q7DGGMGjQ8//HCfqmZHO45wHVTJrLCwkBUrVkQ7DGOMGTREpCTaMUSCHWY0xhgz6EUlmXl7ElknIhtFZIGf508WkSpx+r/7RERujUacxhhjBod+P8woIjE4A7edhnMl+3IR+Zeqru4061vqv9doY4wxpoNo7JnNBDaq6mZVbcbpn+2cKMRhjDHmIBGNZJZHx+EtduB/CItjReRTb6/S3Q23jYhcJSIrRGRFaWlppGM1xhgzCEQjmfkbPqTzldsfAQWqOgVnCPdnuy7iXVD1T6o6XVWnZ2cP+upSY4wxIYhGMttBx7GaRtJpTCtVrVbVWu/9l4BYEQlnRGtjjDEHsWgks+VAkYiMFpE4nOHp/+U7g4gM8w4ih4jMxImzrEtLEdDc1sxfPv8L725f1hfNG2OM6Qf9nszUGR79OpwBHtcAj6vqKhG5WkSu9s52PrBSRD4F7gEu0j7qRDKmsYXR3/k9JX++ry+aN8YY0w+i0gOI99DhS52mPehz/z6gX7JLTHIysbEJpH24sT9WZ4wxpg9YDyBA9VHjGLWllqbqimiHYowxJgSWzICkE2bh9sCW156LdijGGGNCYMkMGD3rTOrioeKNxdEOxRhjTAgsmQEFmWNYNcZN/PJV2GClxhgz+FgyA1zionTqKBIrG2hauzba4RhjjOklS2Ze7mNnAFC9ZEl0AzHGGNNrlsy8xo6dzqZhUP7Gq9EOxRhjTC9ZMvMqzizm47GCZ+Va2iorox2OMcaYXrBk5jU6fTSrihIQj1K7zLq2MsaYwcSSmVeMK4bYw4qpT3ZTt/StaIdjjDGmFyyZ+SjOnsyno6H2rbdQjyfa4RhjjAmSJTMfxZnFLB/toa28nMZVq6IdjjHGmCBZMvMxKWsSn44RVITaN5dGOxxjjDFBsmTmY0zGGJpS4qkcO5TapZbMjDFmsLBk5iPWFcv4IeNZNT6Bxs8/p7WsT8YDNcYYE2GWzDopzirm9RGVoErd229HOxxjjDFBsGTWyaSsSawaWo9kDrHzZsYYM0hEZaTpgaw4qxgVoeaoImTZMrS1FXHbZjLGmIHM9sw6Kcoowu1ys7E4DU9VFQ2ffRbtkIwxxgRgyayTuJg4ijKKeGdEHcTE2KFGY4wZBCyZ+VGcVcwnjRtInDbNSvSNMWYQsGTmx6TMSVQ2VeI5ZipNa9bQsmdvtEMyxhjTA0tmfhRnFQOwfXI2AHVvW8fDxhgzkFky82P8kPHESAyfpVfizs2182bGGDPAWTLzI8GdwJiMMawuX0PKiSdSt2wZ2tIS7bCMMcZ0w5JZN4ozi1ldtprkE0/AU1dH/UcfRzskY4wx3bBk1o1JWZMobyynfso4iI2ldumb0Q7JGGNMNyyZdWNS1iQA1jaVkDT9KOqsRN8YYwYsS2YALY3QWNVh0oQhExCENWVrSDnxJJo2bKRl584oBWiMMaYnlsxam2HhKHj3/g6Tk2KTGJ0+mtVlq0k56UQAat+yEn1jjBmIopLMRGSuiKwTkY0isqCH+WaISJuInN9nwbjjIKMA9q7u8lRxVjGry1cTN3o0sSNHWom+McYMUP2ezEQkBrgfOBOYBMwTkUndzHcH8J8+DyqnGPau6TK5OLOYvfV7KWssc0r033sPT1NTn4djjDGmd6KxZzYT2Kiqm1W1GXgUOMfPfN8FngL6vi+pnElQvhlaGjpMbi8CWVO2hpSTTkQbGqhfsaLPwzHGGNM70UhmecB2n8c7vNP2E5E84DzgwUCNichVIrJCRFaUlpaGFlFOMagH9q3vMHli5kQA1pSvIWnmTCQuzqoajTFmAIpGMhM/07TT47uAm1W1LVBjqvonVZ2uqtOzs7NDiyjHe5Sz06HG1LhUCtIKWF22GldiIklHH23nzYwxZgCKRjLbAYzyeTwS2NVpnunAoyKyFTgfeEBEzu2ziDLHQEyc/yKQzGLWlDlJLuXEE2neupXmkpI+C8UYY0zvRSOZLQeKRGS0iMQBFwH/8p1BVUeraqGqFgJPAteo6rN9FlGMG4ZO8F8EklXMrrpdVDZWknLiCQDULrUSfWOMGUj6PZmpaitwHU6V4hrgcVVdJSJXi8jV/R3Pft1UNLYXgawuX01cQQFxhYU2YKcxxgwwUbnOTFVfUtXxqjpWVX/tnfagqnYp+FDVy1X1yT4PKqcYqrZ36QmkONMZ22z/ocaTTqT+/ffxNDR0acIYY0x0WA8g7fYXgaztMDk9Pp28lDzWlDvJLPnEE9HmZuref7+/IzTGGNMNS2btcpw9MH9FIJOyJrG6zJmeNGMGkphoJfrGGDOAWDJrlz4K4lK67Qlke812qpurccXFkXzssdS+uRTVzlcUGGOMiQZLZu1cLsie2O2eGcDaMucQZMqJJ9KycyfNmzf3a4jGGGP8s2Tmq5uKRt+eQIADJfp2AbUxxgwIlsx85UyC+n1Q27FbrKzELHKTcvefN4sdMYL4oiJq37JkZowxA4ElM1+57RWNPReBACSfeAL1Kz6krbauv6IzxhjTDUtmvrrpoxGcnkBKqkuoa3GSV8qJJ0FLC/XvvdufERpjjPHDkpmv5GxIyvK/Z5Y5CUVZV74OgKQjp+FKTrbzZsYYMwBYMvMl4uyd9dStlfdQo8TGknz88dQutRJ9Y4yJtrCSmYgki4jLe3+8iHxZRGIjE1qUtFc0dkpQ2UnZDE0cur+iEZyurVr37KFp/frOrRhjjOlH4e6ZLQUSvINpLgauAP4WblBRlVMMzTVQtaPLU12KQGZZib4xxgwE4SYzUdV64CvAvap6HjAp/LCiqKcikMxiNldtpqHV6WQ4NjeH+EnF1Lz6an9GaIwxppOwk5mIHAvMB170TnOH2WZ0ZTsXSPsdqDOrGI96WF9x4LBixrnn0fj55zSsWtVfERpjjOkk3GR2I/Bj4BnvmGRjgDfCDyuKEjMgLc/vntnkrMkAHQ41pp97DpKYSOWjj/ZbiMYYYzoKK5mp6puq+mVVvcNbCLJPVa+PUGzRk1MMe7vuaeUm5TIkfsj+sc0AYtLSSPuvs6h64UXaamr6M0pjjDFe4VYzPiIiaSKSDKwG1onIDyMTWhTlFEPpemhr7TBZRCjOKu5Q0QgwZN48tKGBqmef688ojTHGeIV7mHGSqlYD5wIvAfnAJWFH1c8aW9qoaWw5MCFnErQ1QcWWLvNOyprExoqNNLU17Z+WOHkyCUccQcWjj9o1Z8YYEwXhJrNY73Vl5wLPqWoLMKh+zZtbPRzxs1f481s+iauHgTqLM4tp1VY2VmzsMH3IvHk0b9pE/QfL+zJcY4wxfoSbzP4IbAWSgaUiUgBUhxtUf4pzuxiRkcDGvT7nu4ZOAKTHnkBWlXU8p5Z25lxc6elULFrUl+EaY4zxI9wCkHtUNU9Vz1Ln+No24JTIhNZ/xuWksnFv7YEJcUmQOdrvnlleSh6pcaldzpu5EhLIOO88al57jZa9e/s6ZGOMMT4i2jejOloDzzmwFOWmsGVfHS1tngMTu+mjUUSYlDmpQ0VjuyEXXQitrVQ99VRfhmuMMaYT62gYGJedQkubUlJWf2BiTjGUbYKWxi7zT8qaxPqK9bS0tXSYHldYSPJxx1Hx2ONo66DL6cYYM2hZMsPZMwM6HmrMKQZtg7INXeYvziqmxdPCpqpNXZ4b8vV5tH7xBbVvvtln8RpjjOko3OvMYrw95V8vIje13yIVXH8Zm92ezHyKQAL00Qj4PdSYcvLJuIcNo+IRKwQxxpj+Eu6e2fPA5UAWkOpzG1SS493kZSSywXfPLGscuGL9FoHkp+WTHJvcpaIRQNxuMi44n7ply2guKenLsI0xxniF2ynwSFU9IiKRRNm4nJSOhxljYmHoeL97Zi5xMTFzYpeKxnYZ51/Avj88SMVjj5P7o8HfIYoxxgx04e6ZvSwip0ckkigbl5PCptJaPB6fa75ziv3umYG3CKR8Pa2eroUesbk5pM6ZQ9VTT+Fp7FpAYowxJrLCTWbvAc+ISIOIVItIjYgMqoum2xXlpNDY4mFnZcOBiTnFULkNmrp2IFycWUxjWyNbqrp2eQVOjyBtVVVU//vffRWyMcYYr3CT2W+BY4EkVU1T1VRVTYtAXP2uvaJxg78ikNJ1XeZv7wmku0ONSUfPJG7MGOsRxBhj+kG4yWwDsFJ72buuiMwVkXUislFEFvh5/hwR+UxEPhGRFSIyK8w4AxqX7dStbNjTqTwf/B5qLEwrJNGd2GFsM18iwpCLLqLx089oXO1/HmOMMZERbjLbDSwRkR8HW5ovIjHA/cCZwCRgnohM6jTbYmCKqk4FvgH8Jcw4A0pPiiU7Nb5jEUhGAcQmwZ6uySjGFcOEIRP8lufvb9M7cGfFIhu40xhj+lK4yWwLTuKJI/jS/JnARlXdrKrNwKPAOb4zqGqtz95eMv3UE39RTkrH8nyXC7IndlsE0j62mUc9fp8/MHDnCzZwpzHG9KFwOxr+uar+HPgd8Fufxz3JA7b7PN7hndaBiJwnImuBF3H2zvwSkau8hyJXlJaW9v5F+Ggvz+9w1LSbPhrBOW/W0NrA1uqt3bZpA3caY0zfC7cHkMNE5GNgJbBKRD4UkcmBFvMzrcuel6o+o6oTccZK+2V3janqn1R1uqpOz87O7k34XRTlpFDb1Mqe6gMDb5JTDHV7oW5fl/l76gmknQ3caYwxfS/cw4x/Am5S1QJVLQC+D/w5wDI7gFE+j0cCu7qbWVWXAmNFZGiYsQY0NsdfRWN7EUjXhDU2YyzxMfHdFoG0s4E7jTGmb4WbzJJV9Y32B6q6BOccV0+WA0UiMlpE4oCLgH/5ziAi40REvPePxDknVxZmrAEV5Tin+zp2ONx9H41ul5sJQybwWelnPba7f+DOR61M3xhj+kK4yWyziPxURAq9t5/gFIV0yzve2XXAf4A1wOOqukpErhaRq72zfRVYKSKf4FQ+Xtjb8v9QDE2JIyMptmMRSOowSMjotghkVt4sPi39lH0NXQ9Dtts/cOerNnCnMcb0hXCT2TeAbOBp720ocEWghVT1JVUdr6pjVfXX3mkPquqD3vt3qOpkVZ2qqseq6tthxhkUEWFcdgobfa81E+mxCGR2/mwUZcn2JT22bQN3GmNM3wk5mXmvF3tCVa9X1SO9txtVtSKC8fW7otwUNpbWdpyYU+wkMz87h+OHjGdU6ihe2/Zaj+3awJ3GGNN3Qk5mqtoG1ItIegTjibpxOamU1zVTVtuporGpCqq71qmICHPy5/D+7vepae75WjIbuNMYY/pGuIcZG4HPReSvInJP+y0SgUXLuP0VjcEVgQDMyZ9Dq6eVpTuW9tj2/oE7rUcQY4yJqHCT2YvAT4GlwIc+t0GrKKd91Ong+mgEOCL7CLITs1m8bXGPbe8fuPPtt23gTmOMiaCQkpmItP9qT1LVv3e+RTC+fjc8PYHkuJiOySwpE1KGdbtn5hIXs/Nn8/bOt2ls7Xn8sozzLwC3m4rHHo9k2MYYc0gLdc9suIicBHxZRKaJyJG+t0gG2N9EpOuo09DjQJ3gHGpsaG3gnV3v9Ni+DdxpjDGRF2oyuxVYgNN7x+9wxjVrv90ZmdCiZ1xOasdeQMA5b1a6DjxtfpeZPmw6aXFpAQ81gg3caYwxkRZSMlPVJ1X1TOB/VPWUTrfZEY6x343LSWFPdRPVjS0HJuZOgtYGqNjqd5lYVywnjzqZJduX0OJp8TtPu/aBOyutEMQYYyIi3F7zu+0AeDALpQgEnAuoq5urWfHFih7bdwbuvJCGTz+1gTuNMSYCwq1mPCgV5XqTmW9PINkTnb/dFIEAHDfiOBLdiUEdakw/91wkIcHK9I0xJgIsmfkxckgScW5Xx/NmcckwpLDHPbNEdyLHjzie17e93u2Ane1i0tJI/9LZVD3/PC179kQocmOMOTSFncxEJEZERohIfvstEoFFU4xLGJvtr6Kx+z4a280pmENpQymf7/s84HqyrroK2toovevucMI1xphDXriDc34X2AO8inMB9YvACxGIK+rG5aR07AUEnPNmZRuhtcn/QsCJI0/ELW4WlwQ+1Bg3ahSZl11K1bPP0rBqVbghG2PMISvcPbMbgAneHu4P996OiERg0VaUk8LOygbqm306Bc6ZBJ5WJ6F1Iy0ujaOHH83ibYuDGlk669vfJiYjg70L77CRqI0xJkThJrPtQFUkAhloinJSUIXNpXUHJvYw6rSv2fmz2VazjQ2VGwKuJyY1lezrv0v98uXUvNZzz/vGGGP8C3twTmCJiPxYRG5qv0UisGg70OGwTxFIVhG43D0WgYCTzAQJqqoRIOOCC4gbN5a9/3sn2twccszGGHOoCjeZbcM5XxYHpPrcBr2CrGTcLulYBOKOg6xxAffMhiYOZVrOtKDOm4HTAXHuzTfTsm0b5Y88Ek7YxhhzSHKHs7Cq/hxARFKdh1obYJFBI87tonBoMhv2+CkC2fVxwOVn58/mzhV3sr1mO6NSRwWcP+WEE0ieNYt9D/yB9HPOwT1kSKihG2PMISfcasbDRORjYCWwSkQ+FJHJkQkt+sZ1V55fsRWa6/wu025O/hwAXt/2etDry735R3hqa9l3/wO9DdUYYw5p4R5m/BNwk6oWqGoB8H3gz+GHNTAU5aZQUl5PU6tP58LtRSCla3tcdmTqSCZmTuS1kuCLOuKLisj42gVULFpE0+bNoYRsjDGHpHCTWbKqvtH+QFWXAMlhtjlgjMtJoc2jbN1Xf2BigFGnfc3Jn8OnpZ9SWl8a9Dqzv/tdXImJ7P2f/+1tuMYYc8gKu5pRRH4qIoXe20+ALZEIbCAY56/D4SGF4E4IOpkpyhvb3wg4bzt3VhZDr/42tUuWUPdOz2OjGWOMcYSbzL4BZANPA894718RblADxdjsFEQ6lee7YiB7QsDyfIBxGeMoSCsIukS/3ZBLLiE2L489d/wP2uZ//DRjjDEHhDsETIWqXq+qR6rqNFW9QVUrIhVctCXExjBqSJKfbq0C99EIzlAvs/Nn88HuD6hqCv7acld8PDk/+D5N69ZR+fTTvQ3bGGMOOSElMxG5y/v3eRH5V+dbZEOMrqKcFDb566OxZjfUlwdc/tT8U2nVVpbuWNqr9abOnUvitGmU3n0PbbU9V04aY8yhLtQ9s394/94J/NbP7aAxLjeFzaV1tLb5DOnSiyKQw2BX7r0AACAASURBVIYeRk5STq8PNYoIuT9eQNu+fZT9+aApEDXGmD4RUjJT1Q+9d6eq6pu+N2Bq5MKLvnHZKTS3edhW7lvRGHjU6XYucTF71GyW7VxGQ2tDr9adeMQRpH3pS5Q/9BAtO3f2alljjDmUhFsAcpmfaZeH2eaAUpTr9M7VoaIxLQ/i04PaMwM4teBUGtsaeWdn76sTc753I4iw9/d39XpZY4w5VIR6zmyeiDwPjO50vuwNoCyyIUbX2GznsrkORSAizt5ZkMnsqNyjSI9P57Vtve8VP3bECDKvuJzqF16g4dNPe728McYcCkLtm/EdYDcwlI7nyGqAz8INaiBJTYhleHqC/yKQVc+AqpPceuB2uTl55Mm8vv11WtpaiI2J7VUMQ7/1LSqfeoo9ty+kYNEjSID1GWPMoSbUc2YlqrpEVY/tdM7sI1VtDbS8iMwVkXUislFEFvh5fr6IfOa9vSMiU0KJM1L8jzo9CRoroeaLoNqYkz+HmuYaln+xvNfrdyUnk3PDDTR88gk1L7/c6+WNMeZgF25Hw8eIyHIRqRWRZhFpE5HqAMvEAPcDZwKTgHkiMqnTbFuAk7yjVv8Spw/IqBmX43Q47PH4jATdiyIQgGNHHEuiO7HXVY3t0s87j/iJE9l752/xNDWF1IYxxhyswi0AuQ+YB2wAEoErgXsDLDMT2Kiqm1W1GXgUOMd3BlV9x+fi6/eAkWHGGZainFQaWtrYVeVTjRjkqNPtEtwJzMqbxeJti2nz9L5XD4mJIffmH9GyaxflDz/c6+WNMeZgFm4yQ1U3AjGq2qaqDwGnBFgkD9ju83iHd1p3vgl0e2xNRK4SkRUisqK0NPgOfXujKLd91GmfQ43JQyE5J+hkBs4F1GWNZXy2L7TTisnHHkvKKadQ9uAfad23L6Q2jDHmYBRuMqsXkTjgExH5HxH5HoF7zfdXvaB+piEip+Aks5u7a0xV/6Sq01V1enZ2drBx98q4bG+Hw/4G6gzyMCPACSNPwO1yBz0CtT85P/whnqYmSu+9L+Q2jDHmYBNuMrsEiAGuA+qAUcBXAyyzwztfu5HArs4zicgRwF+Ac1Q1quX+Q5LjGJoS53+gztK14PH4X7CT1LhUjhl+DK9tew1Vv/k7oPgxoxkybx6VTzxB4/r1IbVhjDEHm3A7Gi5R1QZVrVbVn6vqTd7Djj1ZDhSJyGjvXt1FQIf+HEUkH6cn/ktUdUD8Yo/NTunYez44e2Yt9VBZEnQ7p+afys7anayvCP1lDb3mO7hSUth7x/+EnBSNMeZgEupF0497/37uU0K//9bTst7S/euA/wBrgMdVdZWIXC0iV3tnuxXIAh4QkU9EZEUocUZSUa5T0dghefSij8Z2J486GZe4QrqAup17yBCGXvMd6pYto+q550JuxxhjDhahXjR9g/fv2aEsrKovAS91mvagz/0rcSojB4yinFSqG1sprWkiJy3BmZg9wfm7dzVMPCuodrISs5iWM43F2xZz7dRrQ44n8+KLqV38Ol/87OckFE8iYcL4kNsyxpjBLtSLpnd7734FaPUebtx/i1x4A0f7qNMdKhoT0iA9v1dFIOBcQL2hYgMl1aFvKnG7yfvdb3GlprDzxhttmBhjzCEt3AKQNOAVEXlLRK4VkdxIBDUQFXmTWZcikJHTYfMSaG0Ouq05+XMAQr6Aup07O5u8O39Lc0kJu3/6Ezt/Zow5ZIVbAPJzVZ0MXAuMAN4UkdBPBg1g2anxpCW4uxaBTJkH9WWw4T9BtzUiZQTFmcVhJzOA5KNnkn3jjdS8/G8q/vlI2O0ZY8xgFPZF0157gS9weszPiVCbA4qIOH00dr7WbOxsSBkGH/+zV+2dWnAqn5V+xp66PWHHlnXlN0k5+WT23HEHDZ8dVP08G2NMUMLtm/E7IrIEWIzTg/63vP0pHpSKclLZVNopmcW4YcqFsOEVqAk+MbUfanxj+xthxyUuFyMW3k5sdjY7bryR1oqKwAsZY8xBJNw9swLgRlWdrKq3qWrvKiEGmaLcFPbVNlNe1+n82NT5oG3w+eNBtzUmfQyFaYVhlej7isnIIO/uu2gr3ceuBQvQIC/kNsaYg0G458wWACkicgWAiGSLyOiIRDYAje2uCCR7AuRNdw41BlmEISKcXng6y79YzubKzRGJL/Hww8n58QLq3lxK2Z//EpE2jTFmMAj3MONtOP0m/tg7KRb4f+EGNVB1W9EIMG0+lK6BXR8F3d784vkkxCRw/yf3RypEhsybR9pZZ1F6993Uvfd+xNo1xpiBLNzDjOcBX8bplxFV3QWkhhvUQDUiPZHE2JiuFY0Ah30V3AnwSfAVhZkJmVwy6RJeKXmFNWXB9yLSExFh2C9+QVxhITt/8ANa9u6NSLvGGDOQhZvMmtW5uEkBRCRQj/mDmssl+wfq7CIhHYq/BJ8/AS2NQbd52eTLSItL496PAw0DF7yYlGRG3n0Xnro6dt30fbQ14ODfxhgzqIWbzB4XkT8CGSLyLeA14M/hhzVwFXWXzACmfh0aq2Ddi0G3lxqXyjcO+wZv7XyLj/d+HKEoIb6oiOE/u436FSsovfueiLVrjDEDUbgFIHcCTwJPAROAW1U1crsYA9DYnBR2VzVS09jS9cnRJ0HayF4dagSYN3EeWQlZ3PPRPRHtxSP9nHPI+NrXKPvzn6l5I/xLAIwxZqCKxEjTr6rqD1X1B6r6aiSCGsjai0A2lfrpC9EVA1PnwabXobrLEG3dSopN4qojrmLFnhW8u/vdSIUKQO5/30L8pGJ23byA5h07Itq2McYMFKEOAVMjItXd3SId5EBSlOvUt2zY46cIBJzurdQDny7qVbvnjz+f4cnDI7535oqPZ+Tdd4MqO2/8Hp7m4PuQNMaYwSLUXvNTVTUNuAtYAOThjBh9M/CryIU38IwakkhcjKv782ZZYyH/uF5dcwYQFxPHd6Z8h1Vlq3h9++sRitbb9qhRjLj9NzSuXMnehQsj2rYxxgwE4R5mPENVH1DVGu9o038AvhqJwAYqd4yLMdnJ3SczcK45K98E2z/oVdtfGvslCtMKue/j+2jztIUZaUepp55K5je+QcUji6h6/oWItm2MMdEWbjJrE5H5IhIjIi4RmQ9E9ld4ABqbk9JxXLPOJp0DsUnwSe+uH3e73Fw77Vo2Vm7k5a0vhxllVznfu5HEo45i92230bRpU8TbN8aYaAk3mX0d+Bqwx3u7wDvtoFaUk8L2inoaW7rJ2/GpMOlcWPkMNPdu0MzTC05nwpAJ3P/x/bR4/FRMhkFiY50BPRMS2HHDDXjqbEBPY8zBIdzS/K2qeo6qDlXVbFU9V1W3Rii2AasoJxVVuvag72vafGiugTW9O6TnEhfXH3k9O2p38OzGZ8OMtKvY3FzyfnsnzZu3sP2aa/HU10d8HcYY098iNZ7ZIWVcT300tss/DjIKen2oEeCEvBOYkj2FBz99kKa2plDD7Fbysccy4o6F1C9fzrarrqKt1vbQjDGDmyWzEBQOTSLGJT0nM5fLGRpmy1KoKOlV+yLC9dOuZ2/9Xh5b+1iY0fqX/qUvkffbO2n4+BO2X3klbTXdXGpgjDGDgCWzEMS7YyjISuo66nRnU+cBAp8+2ut1zBw+k2OGH8NfV/6Vupa+2XNKO/NM8u76PQ2rVrHtim/QVlXVJ+sxxpi+FpFkJiLHiMjrIrJMRM6NRJsD3bjsFP+95/vKyIfRJ8In/4QQBsu8ftr1lDeW8/9W992oOmmnncbIe+6mad06Si6/wkapNsYMSqH2ADKs06SbcIaCmQv8MtygBoOi3BRKyuppbg2QpKbOh8oSKFnW63Ucnn04p4w6hb+v+jtVTX2315R6yimMfOABmjdvZttll9NaVtZn6zLGmL4Q6p7ZgyLyUxFJ8D6uxCnJvxA4qLuzajcuJ4VWj1JSFuAQYPGXID6t150Pt7tu2nXUttTy0MqHQlo+WCknzGLUg3+geds2Si69zMZBM8YMKqF2Z3Uu8AnwgohcAtwIeIAk4JA4zFiU4/TR2GMRCEBcEkw+D1Y/C029L7IYP2Q8Z44+k0fWPsK+hn2hhBq05GOPZdSf/kjL7t1su/QyWvbs6dP1GWNMpIR8zkxVnwfOADKAp4F1qnqPqpZGKriBbGx2CiL03BNIu6nzoaUeVoV23di1U6+lua2Zv3z+l5CW743kmTPJ/8ufaS0tpeTiS2jZubPP12mMMeEK9ZzZl0XkbeB1YCVwEXCeiCwSkbGRDHCgSoyLIS8jMbhkNmomZBWFfKgxPy2fc8edy+PrHmd37e6Q2uiNpCOPJP+h/6OtspKSSy61oWOMMQNeqHtmv8LZK/sqcIeqVqrqTcCtwK8jFdxA1+Oo075EnFGot70DZaH1iXj1lKsBePCzB0NavrcSjziC/L89hKeujpKLL6G5pHfXyhljTH8KNZlV4eyNXQTsrxRQ1Q2qelGghUVkroisE5GNIrLAz/MTReRdEWkSkR+EGGOfK8pNZVNpLW2eIIZ6mXIRiCvkvbNhycO4cMKFPLfxObZWbQ2pjd5KnDyZ/L//DW1qouTiS2javLlf1muMMb0VajI7D6fYo5VediwsIjHA/cCZwCRgnohM6jRbOXA9cGeI8fWLcdkpNLd62F4eRP+GaSNg7Gxn0M4Qh3f55uHfJC4mjgc+eSCk5UORMHEiBQ//HVWl5JJLaVy/vt/WbYwxwQq1mnGfqt6rqg+qam9L8WcCG1V1s6o2A48C53Rqf6+qLgci2218hI3LDaKPRl9T50P1TtjyZkjrG5o4lIuLL+blrS+zrnxdSG2EIr6oiIKHH0ZiYth22eU0rl3bb+s2xphgRKM7qzxgu8/jHd5pg057h8NBFYEATDgLEtKdUahDdNnky0iNS+W+j+8LuY1QxI8ZTcE/HkYSEii57HIaPvusX9dvjDE9iUYyEz/Tgjjp1E1jIleJyAoRWVFa2r9XBaQlxJKbFh/8nllsAhx+Aax9ARoqQ1pnenw6V0y+giU7lvBp6achtRGquIICCv7xMDEpKWydfzHlDz+MashvnTHGREw0ktkOYJTP45HArlAbU9U/qep0VZ2enZ0ddnC9NWVkBovX7qGirjm4BabOh9ZGWPV0yOucXzyfzIRM7v3o3pDbCFXcyJEUPvkEKbNmsec3t7PjO9dYf47GmKiLRjJbDhSJyGgRicOpiPxXFOKIiJtOH09NYyt3vhLkOawR0yC7OKxDjUmxSVx1xFW8/8X7PLn+yZDbCZV7yBBGPnA/ubfcQt2yZWw551zqPvig3+Mwxph2/Z7MVLUVuA74D7AGeFxVV4nI1SJyNTgdGYvIDpwOjH8iIjtEJK2/Yw3GxGFpXHpsAY98sI3PdgRx6FDEGYV65wooDb2I48IJF3J83vH86r1f8e6ud0NuJ1QiQuall1D42KO4EhPZdvkVlN57H9oWWqWmMcaEQw6mcx7Tp0/XFStW9Pt6qxtbmH3nm4wcksjT3zkOl8vfaUEftXvhtxPhuOvgtF+EvN7a5lou/fel7K7dzT/O/AfjhowLua1wtNXWseeXv6TquedImj6dEXf+L7HDOg+sYIwZiETkQ1WdHu04wmWDc0ZAWkIst5w1kU+2V/LEh9sDL5CSA+PPcAbtbGsNeb0pcSncP/t+EtwJXLv42j7viLg7MSnJjLhjIcMX3k7D6tVsOedcal5/IyqxGGMOTZbMIuS8aXnMKBzCHf9eR2V9EMUgU78OtXtg0+Kw1js8ZTj3zb6P8sZybnj9BhpbG8NqLxwZ557L6KeexD1iBDuuuYYvfvMbPM1BFsYYY0wYLJlFiIjw8y8fRmV9M799JYheMorOgKQsZxTqME0eOpmFJy7k832fc8vbt+DR3o9qHSnxo0dT+NijDLnkEioe/gdbL7qIpi1bohaPMebQYMksgiaNSOPSYwv5f++XsHJngJGh3XEwZR6seR42vR72uufkz+H707/PqyWvcs9H94TdXjhccXEM++9bGPnA/bTu3MWWr55P1XPPRTUmY8zBzZJZhH3vtPFkJcfx0+dW4gnUAfHJCyB7IjxxOezbGPa6L510KReMv4C/rvwrT61/Kuz2wpU6ezajn3uWhEnF7Lp5AbtuXoCnLsDI3MYYEwJLZhGWnhjLgjOL+XhbJU9+FGAcsPhUmLcIJAYWXRRyryDtRIRbjr6F40c4Jfvv7X4vrPYiIXbYMAr+9jeGXnstVc8/z5avfJWGlauiHZYx5iBjyawPfGVaHkcVDOGOl9dSVR+gr+QhhXDhP6BiCzz1zZB71G/ndrm586Q7KUwv5KY3bmJTZWjjp0WSuN1kf/c68h96CE9DA1u/9jV2/eQntOzdG3hhY4wJgiWzPuByCb84ZzIV9c387tUgLowunAVn3QkbX4NXbw17/SlxKdw/537iYuKiWrLfWfLRMxnzr+fIvOQSqp77F5vOmEvpfffjqQ9iCB1jjOmBJbM+MnlEOpccU8A/3ith1a4AxSAA06+Amd+Gd++Dj/4R9vpHpIzgvjn3UdZQFvWSfV8xGRnk/ngBY198gZSTTmLfffex6Yy5VD75pPUeYowJmSWzPnTT6RMYkhTHrc+tClwMAnDGb2DMyfDC92Bb+Oe7Dht6GAtPcEr2//vt/45qyX5ncfn5jLzr9xQ88gixeXns/slP2XLeV6h96+1oh2aMGYQsmfWh9MRYbj5zIh+WVPD0xzsDLxDjhvMfgoxR8NjFUBlEbyIBzCmYw01H3cQrJa9EvWTfn6Qjp1Gw6BHy7vo9noYGtn/rW2z75pU0ruu/wUeNMYOfJbM+dv6RI5mWn8HCl9dQ1RDEwNlJmTDvMWhthkXzoCnIsdJ6cNnky/aX7D+z4Zmw24s0ESFt7lzGvPgCOQtupmHlSracex67/vu/adljRSLGmMAsmfUxl0v45TmHUV7XzO9fDaJnEIDs8XD+/8HeVfDs1eAJ7/CgiPDjo3/McSOO4xfv/mJAlOz744qLI+vyyxn3n3+TedllVP3reTbNnUvpvffZ9WnGmB5ZMusHh+WlM//oAh5+dyurd1UHt1DRqXDaL50eQt5cGHYMsa7YDiX7mys3h91mX4nJyCB3wc0HikTuv5+Nc+dS8cQTViRijPHLklk/+cHpE8hIiuO2f60k6GF3jr0Wpl4Mb94BK0Mfmbpdalzq/pL9axZfw566PWG32Zf2F4kseoS4vJF88dNb2XzOOVQ8/riV8xtjOrBk1k/Sk2K5ee4Elm+t4JlgikHAGcjz7N/BqKPh2Wtg1ydhx9Fesl/eWM75z5/P4pLweu3vD0nTDhSJSIybL269jQ0nn8KehXfQvG1btMMzxgwANjhnP/J4lK/84R12VDTw+g9OIi0hNrgFa/fCn04BFL71BqTmhh3L5qrNLFi6gDXla/hK0Ve4ecbNJMUmhd1uX1NVGj76iIp//pPqV16FtjaSTzyBzPnzSZ41C3HZ/2fG9IYNzml6rb0YpKyuKfhiEHAG85y3CBoq4LH50BL+BdBj0sfwz7P+yZWHX8kzG57h/OfP57PSz8Jut6+JCElHHUXe737HuMWLGXrNNTSuXs32q77NpjPPpOxvf6OtOsjzksaYg4Yls352+Mh0vj4zn4ffLWHtF7340R1+BJz3IOxYDi/cCBHYo46NieWGI2/gobkP0eZp49KXL+UPn/6BVk/oo1/3p9jcHLK/ex1Fixcz4rd34s4ayt6Fd7DhpJPZfdvPaFzXi38YjDGDmh1mjILK+mZOuXMJRTmpPPbtYxCR4Bdecgcs+Q2c9gs4/oaIxVTTXMOv3/81L25+kSnZU7h91u2MShsVsfb7S+Pq1ZQ/8gjVz7+ANjWRNGMGQ+bPJ3XObCQ2yMO6xhxCDpbDjJbMouTRD7ax4OnPuevCqZw7LS/4BVWd8c9WPwdffwzGnxHRuF7a/BK/eu9XtGkbC2Yu4Nxx5/Yu2Q4QbZWVVD71NBWLFtGyYwfu3FwyLvwa6WefTVx+frTDM2bAsGQ2AA2mZObxKOf94R02l9Zy02njmX90AXHuII/6NtfDQ3OhbDOc8WuYdjG4YiIW2+7a3dzy9i2s2LOC0wpO49ZjbiUjISNi7fcnbWujdulSKv75CHVvO/0+xk+cSOppp5J62mnEFxUNymRtTKRYMhuABlMyAygpq+PHT3/OO5vKyM9M4odnTODsI4YH9+NatROe/AZsfw+GHQ5zFzpDyURIm6eNv6/+O/d+fC+Z8Zn8ctYvOW7EcRFrPxpadu6k+tVXqXn1NRo++ghUiSssJPW000g9/XQSDptsic0cciyZDUCDLZmBU2r+5vpSFr68lrVf1DBlZDoLzizm2LFZwSwMq56GV2+Dqu1Q/GU4/ZfOgJ8RsrpsNQveWsCWqi1cXHwxNx51I/Ex8RFrP1pa9u6l9vXXqXnlVerefx/a2nAPH07qaaeSdvrpJE6bhsREbm/XmIHKktkANBiTWbs2j/LMxzv57Svr2F3VyOyJOSw4cyLjc1MDL9zSAO/cC2//HjytTs8hJ3wf4oNYNggNrQ38bsXveHTdoxQNKWLhCQsZP2R8RNoeCNoqK6l5Ywk1r7xC3bJlaHMzMVlZpM6ZQ+rpp5N89EwrHjEHLUtmA9BgTmbtGlvaeGjZVh5YspG6plYuOGoU3zttPMPSEwIvXL0LXvs5fPYopOTCnFthytchQhcSL92xlFuX3Up1czWXTb6MCydcyLDkYRFpe6Boq62jbumbVL/6KrVvLkXr63GlpZF6yskkzzqBpJkziM0N/6J1YwYKS2YD0MGQzNpV1DVz3xsb+ce7JbhccOWsMXz7pDGkBtNryI4V8O8FzjVpw6c459MKInO+q6yhjNs/uJ1Xtr6CS1zMzp/NvInzmJ47/aA73+RpbKTunXeoeeVVat54A0+VM2J4bH4+STOmkzRjBskzZhCb14tqVGMGGEtmA9DBlMzabS+v585X1vHcJ7vITI7j+tnj+HowlY+q8PmT8NptUL0TJp/nXJuWEZmy9B01O3h8/eM8veFpqpqqGJcxjnkT53H2mLMHRbdYvaVtbTSuWUv98uXO7cMPDyS3ESNImjGDpJkzSJoxg9hRow66xG4OXpbMBqCDMZm1+3xHFbe/vIZ3NpVRkJXEj86YyFmHDwv8o9lcB8vugWV3AwrHfReOvxHiUyISV2NrIy9veZlFaxexpnwNKbEpnDvuXC6ccCGF6YURWcdApB4PTRs2UP/B8v0Jrq2iAgB3bq6T3Ly3uNGFltzMgGXJbAA6mJMZdK18PCwvjdOKhzGjcAhT8zNIinN3v3DVDnjtZ/D5E5A6HObcBoefDzGRKWxQVT4t/ZRFaxfxSskrtHpaOX7E8cybOI9ZebOIieB1cAORqtK8adP+xFa3fDltpfsAiMkeSuKUKSQUFzu3iRNxDw/yEgxj+pgls3BWKjIXuBuIAf6iqgs7PS/e588C6oHLVfWjQO0e7MmsXZtHefqjHfzfsq2s/aIaVXC7hMl56cwoGML0wkymFw5haIqfEvrtH8DLN8OujyA+DcacBGPnwLg5ETsEua9hH0+uf5In1j3B3oa95KXkcdGEiziv6DzS49Mjso6BTlVp3rrVm9xW0LhyJc1bt+7vUzMmPZ14b2JLKJ5I/MRi4seMtqpJ0+8smYW6QpEYYD1wGrADWA7MU9XVPvOcBXwXJ5kdDdytqkcHavtQSWa+qhpa+GhbBSu2lrN8awWfbK+kudUDwJihyUwvdJLbjMJMCrOSnL0BjwfW/xvWvwwbX4fqHU5jQ8fDuFOd5FZ4PMQmhhVbi6eF17e9zqK1i/hwz4fEx8TzX2P+i/PGncfkoZOJdR1aP9yeujoa16+nae1aGtespXHtWprWrUObmgCQ2Fjii4qIL55IQvEkJ8lNmEBMSmQOCRvjjyWzUFcocizwM1U9w/v4xwCqervPPH8ElqjqIu/jdcDJqrq7p7YPxWTWWVNrGyt3Vu9PbitKyqmsbwFgaEoc0wsy9ye4sdnJpMa7oXQdbFoMG1+DrcugrQncCU4F5Ng5ToLLnuAMFhqideXreHTdo7y4+UUaWhtIdCcyLWcaM4bNYHrudCZnTSY2Qoc8BxNtbaV561ZvcltD05q1NK5Zs//8G0BsXh5xBQXEFRYQV1BAbH4+cQWFxI3MQ+Liohi9ORhYMgt1hSLnA3NV9Urv40uAo1X1Op95XgAWqurb3seLgZtVtcdMZcmsK49H2VRa6yS2reUsLylne3nD/ufTEtyMyEhk5JBE8jISyU+Fw1pXMbrqPTJ3v4273DuMSlqecyhy3Kkw+iRIDK2vxurmat7b9R7Lv1jOij0r2Fi5EYBEdyJTs6cyfdh0ZgybwWFZhx2SyQ2cQ5Ste0tpXLOaprVradqwkeaSEppLSvDU1ByY0eVyEl1+ftdkN3KkHbI0QbFkFuoKRS4AzuiUzGaq6nd95nkRuL1TMvuRqn7op72rgKsA8vPzjyopKemHVzG4fVHVyMfbKthWXs/OygZ2VjQ4fysbqGnsOJZZQUw5Zyev5iTXpxze/AmJnjo8EkNDaiEt6QVoxmjcQ8cSnzOWuOxxznk3d/B7C+WN5Xy450NWfLGC5XuWs6FiAwAJMQlMyZnC9FwnuR0+9HDiYg7tvRBVpa2ykuatW/cnt5aSbQcSXW3tgZljYpxEN2oUsSOG484dRuyw3AN/hw3DlZJiRSjGklnIK7TDjANadWMLu7wJbldlAzsqG9hV2cjOinr2VNQxvG4lJ7g+Y4LsoED2kC97SJam/cu34aJUhrLHPYLy+DyqEkZSnzKK5tQCPBmFJKZmkBzvJsHtIj425sDfWBfx7hiaPNWsrfyUlWUf83Hph2yoWI+ixMfEMyV7ClNzpjI6fTT5qfnkp+YP2t78I01VaauooHlriTe5baVl2zaat22nZc8XtO0r6zKgqyspCfewYbhzc4jNHYZ7WC6xw4bhzvX+HTaMmPR09N9JugAADpJJREFUJEI9yASKXxU8qngUlE6Pff52nq50eux9fv9ffJfv+Bx0XQd0ndd3PZ3bdR77X17pGNeB13WgzQ5t0HF96tOu77wd2kFJjnNz2XGFIW17S2ahrlDEjVMAMgfYiVMA8nVVXeUzz38B13GgAOQeVZ0ZqG1LZn2vudXDF1WNlNc3U9XQQnV9M83Ve5CKzcRVbSOxdhupDdvJbNpBdssu0rXjaNqlmsZuzaJKk6kimWrv3ypNppKU/dPb/9a5Y2hN2UtM8lY0YRMe906QA59ZlyYR68khnhwSyCGBXJIkhyTXcBIkFbfbRYwIbpcQ4xJEwCXiPf0nuMQ5FSj77zt7Ku3zCOByCZ33Xzp/azp/j3wf7v9R8v4Atc/feXr7D6Fqx+ntP6J0+LHvOM33Bx46/jh7VHG1tZJcU0FKdQWpteWk1pSTWlNBWk05abUVzq2uElen19EmLuoTkqlJSKUmIYVa79+ahBSqElKoiU+hKj6F6vhkquNTqI5NxiOyP5aucftPSv1DcaG48OBCEZQYPH4eK+L92z6/SNdlfR/7XWb/fUWkfZme5xfoME+HmL1tdGjXe4tLSOLWn/4mpK1ysCSzHi5M6huq2ioi1wH/wSnN/z9VXSUiV3uffxB4CSeRbcQpzb+iv+M0/sW5XeRnJZGf5dvLRx5wpP8FGquhYgtavoWW0o0k79vM2OrdSGMVrqZSXE0bcTdX4/K0dLtOT4OLxuYU6l2pVLkS2eF2s90dw0437IpRdrv3sDdmJ6UxLahP1olriyG9KYmU1hSSWlNJaEnF3ZZIrCbgak0gpi0RNBYPLtrURRsu2hTvXxdtKrTholWd6Sod91A6J7jOR+ykw3Oy/0fN+SECV/t9gRhvgt7/4ybOD1uMtP/Iwv9v72xjJDnKO/77V/fM7mIb3x1nE+dwAgYHJViyOSyLkIAsJXHAIjgggogIseJICAESIIHiyApyPiUEkg8hIW8KASICBhknVoKFISECIUyAi20OGbCB82H7OBN8673bm7fufvKhambn5mb2dtc7b7vPb1Wql6dq+pmna+rZqu6uDqGijpFha+XpM3KMoKrXNvRkFQHIVMbPeHpFuLA7aNcpdDHLXMQKFaEqWWi0qJ1qUF9tkq82qTXaZM02extN9jdOEp44Smh0CO1y6LkyAQsZLGWwEFA9pFhoQTGuB0LdCAsi1GKsOmS1ihCIg7ylwd2is1E330unuK+erIRuGkNWIqvOKNup2NMuAbbmzHYK/tC0M33MoHMaGsvQXB4Sn1hLN1fi3ZZFC4omFO0Ut2gXTR6l4IcqeTiDo7Wco3nO0VrOY3lONeT6UM2MvWXJnrJib1WxpyzZU1XsLSv2VGWMU/r8ylg0Y9Eqlioj52yHtmNQlqasWXzxq8JaWcgwCxStQNnMKFuiaImyIYqmKJtQNqBsQ9kyqpbFuG1nT2kHCPVAWMwIC1lM17vpPKXzXtBCTliorYXFGmGxTqjnaKGG6jmhXke1Wnydj0L6LkrfJ/R9r7W8SZQSFVAqutBSUCFKoJIosSSP5RVQYWvtsNiuWw6pDr22vTYY1RllqdwqDCitK6v6ZIb1YliqLfGGK9+0tVPtMzPH2SYkqJ8Xw4Vb37S3DjwnBaqqz+m16LRPcuzkozzRWma5/SQnWk/GuH2S5fYKJzonWW6f4judk5zonGKlaGDnGHkzAouhxmJWYzHUWAo1FkM9lqX8UsrXQ06mLIYQyMnIQ06mQB6yniwPObkysrCWDwooBRQA9eWFlJ0pl3ppIUwBE2uLUgJTd56iuMDVK4vuubIqLQn2xWaUVvbSFRWVrYXBstLKM9pUZUlotAirDbLVJtlqi/x0i2y1SW21Td5oka+2qZ1uU2t0yFoFtVZBfqogb7WotcoY2mX/SvOGKDJo10QnF50c2rlo16CTQbsGrczo5NDKjU4W63dDzCvmA3RyeukiGwyiCtHJlRmU4cxQhdiuCmfLLGz9X6OLly7esjPbKbgzc3YmIUBY6j34XeOZ/MwznsdG9zgpqoKV9grLzWVOtE6w3FzmdHGaRtGgWTRplk2aRTPmU7pZNGmUUX6iaHKsaNLsLNMoGnTKDoUVlFVJYQWVVeP77jOGEJkygkIvZMpQXWQLGWFfyqu/3lKvXgihV54pIyAWCrHYhsU2LLSNhU4V45ZR7xi1TkVeGLWUzrplnYq8qMjaFYudivM6JVk3NCqydkEoKlRUhKKMoROXLydCCDFkKQ4Z6qVDnGGmWH1l+UUXwesmo+Ks4s7McYaQh5x9i/vYt7hvLJ/fnbmUVUlpJUVVUFRFr6ywlE9yoDdTtLTE1Cuz0TIzS9frApIQ8a4Wpb+QrgN2Zb2y3k0xaw6o+xldxzNYFmeQa+XddvN++7+ZQVli7TbW6ZwZ+svabawosaKAssDKEuucmbayiJ81mC4KqEqsqqCssKpciyuLsrJai8sSs7W62QVPn7aZpo47M8eZAt2Bfrdt6TWPSII8R7kPl7PM+B8gcRzHcZwx487McRzHmXvcmTmO4zhzjzszx3EcZ+5xZ+Y4juPMPe7MHMdxnLnHnZnjOI4z97gzcxzHceaeHbXRsKQfA1t9O+d+4P+2UZ3twvXaHK7X5nC9NsdO1Otnzeyi7VRmGuwoZ/ZUkPT1Wdw52vXaHK7X5nC9NofrNbv4MqPjOI4z97gzcxzHceYed2Zr/P20FRiB67U5XK/N4XptDtdrRvFrZo7jOM7c4zMzx3EcZ+5xZ+Y4juPMPbvKmUl6uaTvSHpI0s1D5JL0l0l+v6SDE9LrUklfkPSApG9JevuQOtdKelLSvSm8Z0K6HZH0zXTMrw+RT9xmkp7fZ4d7Ja1IesdAnYnYS9KHJD0u6XBf2T5Jn5P0YIr3jmi7bn8cg17vk/TtdJ7ukLRnRNt1z/kY9LpV0qN95+r6EW0nba/b+nQ6IuneEW3Haa+hY8Ms9LGZw8x2RQAy4HvAZUAduA/4hYE61wN3AQJeDHx1QrpdAhxM6QuA7w7R7Vrg36dgtyPA/nXkU7HZwHn9EfHBz4nbC3gZcBA43Ff2Z8DNKX0z8N6t9Mcx6HUdkKf0e4fptZFzPga9bgXetYHzPFF7Dcj/HHjPFOw1dGyYhT42a2E3zcyuAR4ys++bWRv4BHDDQJ0bgI9a5B5gj6RLxq2YmR0zs0MpfRJ4ADgw7uNuE1OxWR+/AnzPzLa688tTwsy+CDwxUHwD8JGU/gjwm0OabqQ/bqteZna3mRUpew/wrO063lPRa4NM3F5dJAl4HfDx7TreRllnbJh6H5s1dpMzOwD8sC//CGc7jI3UGSuSng28EPjqEPEvSrpP0l2SXjAhlQy4W9I3JL1piHzaNns9oweZadgL4JlmdgziYARcPKTOtO12E3FGPYxznfNx8La0/PmhEUtm07TXS4HjZvbgCPlE7DUwNsxDH5sou8mZaUjZ4HMJG6kzNiSdD9wOvMPMVgbEh4hLaVcCHwD+dUJq/ZKZHQReAbxV0ssG5FOzmaQ68CrgU0PE07LXRpmm3W4BCuBjI6qc65xvN38DPBe4CjhGXNIbZJq/zd9m/VnZ2O11jrFhZLMhZTv2Wazd5MweAS7tyz8LeGwLdcaCpBqxs37MzD49KDezFTM7ldKfAWqS9o9bLzN7LMWPA3cQly76mZrNiIPHITM7PiiYlr0Sx7tLrSl+fEidqdhN0o3AK4E3WLqwMsgGzvm2YmbHzaw0swr4hxHHm5a9cuA1wG2j6ozbXiPGhpntY9NiNzmzrwGXS3pO+o/+9cCdA3XuBH433aH3YuDJ7lR+nKQ1+X8EHjCzvxhR56dSPSRdQzx3PxmzXudJuqCbJt5AcHig2lRslhj5H/M07NXHncCNKX0j8G9D6mykP24rkl4O/AHwKjM7PaLORs75duvVf4311SOON3F7JX4V+LaZPTJMOG57rTM2zGQfmyrTvgNlkoF45913iXf43JLK3gy8OaUF/HWSfxO4ekJ6/TJx+n8/cG8K1w/o9jbgW8Q7ku4BXjIBvS5Lx7svHXuWbPY0onO6sK9s4vYiOtNjQIf4n/DvA88A/hN4MMX7Ut2fBj6zXn8cs14PEa+hdPvY3w7qNeqcj1mvf059537iYHvJLNgrlX+426f66k7SXqPGhqn3sVkLvp2V4ziOM/fspmVGx3EcZ4fizsxxHMeZe9yZOY7jOHOPOzPHcRxn7nFn5jiO48w97swcZ0JI2iPpLdPWw3F2Iu7MHGcCSMqAPcCmnFl6GN1/p45zDvxH4jhDkHRLeg/U5yV9XNK7JP23pKuTfL+kIyn9bElfknQohZek8mvTu6j+hfhQ8J8Cz03vvXpfqvNuSV9Lm+z+cd/nPSDpg8Q9Ji+V9GFJhxXfm/XOyVvEcWabfNoKOM6sIelFxK1/Xkj8jRwCvrFOk8eBXzOzpqTLibtJXJ1k1wBXmNkP0q7nV5jZVek41wGXpzoC7kyb1B4Fng/8npm9JelzwMyuSO2GvlTTcXYz7swc52xeCtxhaf9CSefaz64G/JWkq4AS+Lk+2f+Y2Q9GtLsuhf9N+fOJzu0o8LDF98MBfB+4TNIHgP8A7t7k93GcHY87M8cZzrB93grWluYX+8rfCRwHrkzyZp9sdZ1jCPgTM/u7MwrjDK7XzsxOSLoS+HXgrcQXRd60kS/hOLsFv2bmOGfzReDVkpbSjui/kcqPAC9K6df21b8QOGbxFSZvJL6ufhgngQv68p8FbkrvqkLSAUlnvWQxvbommNntwB8BB7f0rRxnB+MzM8cZwMwOSbqNuEP5w8CXkuj9wCclvRH4r74mHwRul/RbwBcYMRszs59I+rKkw8BdZvZuST8PfCW9reYU8DvEpcp+DgD/1HdX4x8+5S/pODsM3zXfcc6BpFuBU2b2/mnr4jjOcHyZ0XEcx5l7fGbmOI7jzD0+M3Mcx3HmHndmjuM4ztzjzsxxHMeZe9yZOY7jOHOPOzPHcRxn7vl/ph/Th/yj9icAAAAASUVORK5CYII=\n", "text/plain": [ "
" ] @@ -312,7 +312,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 8, "metadata": { "scrolled": true }, @@ -326,17 +326,25 @@ " max error for fisher is 2.50E-03\n", " max error for wnkpc is 5.08E-08\n", "On iteration 1\n", - " max error for asset_mkt is 2.17E-05\n", + " max error for asset_mkt is 2.66E-04\n", " max error for fisher is 1.56E-06\n", - " max error for wnkpc is 1.97E-05\n", + " max error for wnkpc is 2.15E-05\n", "On iteration 2\n", - " max error for asset_mkt is 1.16E-07\n", - " max error for fisher is 3.00E-08\n", - " max error for wnkpc is 8.55E-08\n", + " max error for asset_mkt is 7.57E-06\n", + " max error for fisher is 9.69E-08\n", + " max error for wnkpc is 6.57E-07\n", "On iteration 3\n", - " max error for asset_mkt is 1.13E-09\n", - " max error for fisher is 6.92E-11\n", - " max error for wnkpc is 5.40E-10\n" + " max error for asset_mkt is 4.02E-07\n", + " max error for fisher is 2.25E-09\n", + " max error for wnkpc is 1.64E-08\n", + "On iteration 4\n", + " max error for asset_mkt is 2.20E-08\n", + " max error for fisher is 1.07E-10\n", + " max error for wnkpc is 7.47E-10\n", + "On iteration 5\n", + " max error for asset_mkt is 1.23E-09\n", + " max error for fisher is 5.47E-12\n", + " max error for wnkpc is 3.73E-11\n" ] } ], @@ -354,14 +362,14 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 9, "metadata": { "scrolled": true }, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -393,7 +401,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -405,13 +413,17 @@ " max error for fisher is 2.50E-04\n", " max error for wnkpc is 5.08E-08\n", "On iteration 1\n", - " max error for asset_mkt is 2.06E-06\n", + " max error for asset_mkt is 3.20E-06\n", " max error for fisher is 1.71E-08\n", - " max error for wnkpc is 2.09E-06\n", + " max error for wnkpc is 2.10E-06\n", "On iteration 2\n", - " max error for asset_mkt is 6.73E-09\n", - " max error for fisher is 2.65E-10\n", - " max error for wnkpc is 7.61E-09\n" + " max error for asset_mkt is 7.70E-08\n", + " max error for fisher is 2.57E-10\n", + " max error for wnkpc is 5.83E-09\n", + "On iteration 3\n", + " max error for asset_mkt is 8.06E-10\n", + " max error for fisher is 1.54E-12\n", + " max error for wnkpc is 5.69E-11\n" ] } ], @@ -422,12 +434,12 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -474,7 +486,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.4" + "version": "3.8.3" } }, "nbformat": 4, diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index c4d8ef4..d1e65aa 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -116,8 +116,8 @@ def income(e_grid, tax, w, N): # A potential hetoutput to include with the above HetBlock @hetoutput() -def adjustment_costs(a, a_grid, r, chi0, chi1, chi2): - chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, r, chi0, chi1, chi2) +def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): + chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) return chi @@ -237,8 +237,8 @@ def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) I = K - (1 - delta) * K(-1) + k_adjust - div = Y - w * N - I - return psip, I, div + div = Y - w * N - I - psip + return I, div @simple From b2266707ab12cd5a26d8bef7857b69b6ffa970d7 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 4 May 2021 10:18:43 -0500 Subject: [PATCH 131/288] Implement missing uncapitalize method for working with hetoutput --- sequence_jacobian/utilities/misc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py index da5f2e2..cf47e7b 100644 --- a/sequence_jacobian/utilities/misc.py +++ b/sequence_jacobian/utilities/misc.py @@ -98,6 +98,10 @@ def unprime(s): return s +def uncapitalize(s): + return s[0].lower() + s[1:] + + def list_diff(l1, l2): """Returns the list that is the "set difference" between l1 and l2 (based on element values)""" o_list = [] From f70e6da2356806abd56cb785e89aa0b443f029d8 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 4 May 2021 18:09:18 -0500 Subject: [PATCH 132/288] Upgrade steady_state code to allow for top-level unknowns to supercede unknowns in SolvedBlocks contained within the DAG --- sequence_jacobian/blocks/solved_block.py | 12 +++++-- sequence_jacobian/steady_state/drivers.py | 43 +++++++++++++++-------- tests/conftest.py | 5 +-- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py index 44c2df3..3a2caa7 100644 --- a/sequence_jacobian/blocks/solved_block.py +++ b/sequence_jacobian/blocks/solved_block.py @@ -84,7 +84,8 @@ def jac(self, ss, T=None, shock_list=None, **kwargs): DeprecationWarning) return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, calibration, helper_blocks=None, consistency_check=True, ttol=1e-9, ctol=1e-9, verbose=False): + def steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, + consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: @@ -92,7 +93,14 @@ def steady_state(self, calibration, helper_blocks=None, consistency_check=True, calibration=calibration) self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] - return super().solve_steady_state(calibration, self.unknowns, self.targets, solver=self.solver, + # Allow override of unknowns/solver, if one wants to evaluate the SolvedBlock at a particular set of + # unknown values akin to the steady_state method of Block + if unknowns is None: + unknowns = self.unknowns + if solver is None: + solver = self.solver + + return super().solve_steady_state(calibration, unknowns, self.targets, solver=solver, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 1ea83b3..bd56163 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -82,6 +82,7 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) helper_indices = np.arange(len(blocks), len(blocks_all)) + helper_outputs = {} if sort_blocks: dep = graph.construct_dependency_graph(blocks, graph.construct_output_map(blocks, helper_blocks=helper_blocks), @@ -90,11 +91,9 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, else: topsorted = range(len(blocks + helper_blocks)) - def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): + def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, consistency_check=False): ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) - helper_outputs = {} - # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper # block subsetting # Progress through the DAG computing the resulting steady state values based on the unknown_values @@ -104,8 +103,9 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): continue # Want to see hetoutputs elif hasattr(blocks_all[i], 'hetoutput') and blocks_all[i].hetoutput is not None: - outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) - ss_values.update(outputs.difference(helper_outputs)) + outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, + consistency_check=consistency_check, verbose=verbose, **block_kwargs) + ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) else: outputs = eval_block_ss(blocks_all[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) @@ -115,7 +115,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): else: # Don't overwrite entries in ss_values corresponding to what has already # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(outputs.difference(helper_outputs)) + ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" @@ -135,12 +135,12 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): tol=ttol, verbose=verbose, fragile=fragile) # Check that the solution is consistent with what would come out of the DAG without the helper blocks - if consistency_check: + if consistency_check and helper_blocks: # Add the unknowns not handled by helpers into the DAG to be checked. unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) cresid = abs(np.max(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), - include_helpers=False))) + include_helpers=False, consistency_check=True))) run_consistency_check(cresid, ctol=ctol, fragile=fragile) # Update to set the solutions for the steady state values of the unknowns @@ -151,9 +151,18 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True): def eval_block_ss(block, calibration, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" - input_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - return block.steady_state({k: v for k, v in input_dict.items() if k in block.inputs}, - **{k: v for k, v in kwargs.items() if k in misc.input_kwarg_list(block.steady_state)}) + # Add the block's internal variables as inputs, if the block has an internal attribute + input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel + + # If evaluating a CombinedBlock or SolvedBlock do not numerically solve for unknowns again just evaluate the + # block at the provided values in `calibration` + valid_input_kwargs = misc.input_kwarg_list(block.steady_state) + input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} + if "consistency_check" in kwargs and kwargs["consistency_check"] and "solver" in valid_input_kwargs: + input_kwarg_dict["solver"] = "solved" + input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} + + return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, @@ -248,9 +257,12 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi tol=tol, verbose=verbose, **solver_kwargs) unknown_solutions = list(unknown_solutions) elif solver == "solved": - # If the model does not require a numerical solution then return an empty tuple for the unknowns - # that require a numerical solution - unknown_solutions = () + # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution + # simply call residual_f once to populate the `ss_values` dict and verify if the targets are hit + if not np.all(np.isclose(residual_f(*unknowns.values()), 0.)): + raise RuntimeError("The `solver` kwarg was set to 'solved' even though the residual function indicates that" + " the targets were not hit.") + unknown_solutions = unknowns.values() else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") @@ -265,7 +277,8 @@ def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unkn with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, # to populate the `ss_values` dict with the unknown values that: - # a) are handled by helper blocks and b) are excludable from the main DAG. + # a) are handled by helper blocks and b) are excludable from the main DAG + # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) diff --git a/tests/conftest.py b/tests/conftest.py index 552eed2..ab85a96 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,16 +9,13 @@ @pytest.fixture(scope='session') def rbc_dag(): blocks = [rbc.household, rbc.mkt_clearing, rbc.firm] - helper_blocks = [rbc.steady_state_solution] rbc_model = create_model(blocks, name="RBC") # Steady State calibration = {"eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11, "L": 1.} unknowns_ss = {"vphi": 0.92, "beta": 1 / (1 + 0.01), "K": 2., "Z": 1.} targets_ss = {"goods_mkt": 0., "r": 0.01, "euler": 0., "Y": 1.} - ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="solved", - helper_blocks=helper_blocks, - helper_targets=["goods_mkt", "r", "euler", "Y"]) + ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="hybr") # Transitional Dynamics/Jacobian Calculation exogenous = ["Z"] From a5a94e2be32a98195632bb2baa48e141f434e37f Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 4 May 2021 18:09:34 -0500 Subject: [PATCH 133/288] Remove deprecated helper conventions from debugging code --- sequence_jacobian/devtools/debug.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/sequence_jacobian/devtools/debug.py b/sequence_jacobian/devtools/debug.py index 420a136..48e22ec 100644 --- a/sequence_jacobian/devtools/debug.py +++ b/sequence_jacobian/devtools/debug.py @@ -18,15 +18,12 @@ def ensure_computability(blocks, calibration=None, unknowns_ss=None, # Check if unknowns and exogenous are not outputs of any blocks, and that targets are not inputs to any blocks if exogenous and unknowns and targets: ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous + unknowns, targets, - verbose=verbose, fragile=fragile, - ignore_helpers=ignore_helpers, - calibration=calibration) + verbose=verbose, fragile=fragile) # Check if there are any "broken" links between unknowns and targets, i.e. if there are any unknowns that don't # affect any targets, or if there are any targets that aren't affected by any unknowns if unknowns and targets and ss: - ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile, - ignore_helpers=ignore_helpers, calibration=calibration) + ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile) # To ensure that no input argument that is required for one of the blocks to evaluate is missing @@ -49,10 +46,8 @@ def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unknowns, targets, - verbose=False, fragile=True, - ignore_helpers=True, calibration=None): - cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks, ignore_helpers=ignore_helpers, - calibration=calibration) + verbose=False, fragile=True): + cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks) invalid_xu = [] invalid_targ = [] for xu in exogenous_unknowns: @@ -72,10 +67,9 @@ def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unk print("The provided exogenous/unknowns and targets are all valid candidates for this DAG.") -def find_candidate_unknowns_and_targets(block_list, verbose=False, ignore_helpers=True, calibration=None): - dep, inputs, outputs = graph.block_sort(block_list, return_io=True, ignore_helpers=ignore_helpers, - calibration=calibration) - required = graph.find_outputs_that_are_intermediate_inputs(block_list, ignore_helpers=ignore_helpers) +def find_candidate_unknowns_and_targets(block_list, verbose=False): + dep, inputs, outputs = graph.block_sort(block_list, return_io=True) + required = graph.find_outputs_that_are_intermediate_inputs(block_list) # Candidate exogenous and unknowns (also includes parameters): inputs that are not outputs of any block # Candidate targets: outputs that are not inputs to any block @@ -89,12 +83,10 @@ def find_candidate_unknowns_and_targets(block_list, verbose=False, ignore_helper return cand_xu, cand_targets -def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True, - calibration=None, ignore_helpers=True): +def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True): io_net = analysis.BlockIONetwork(blocks) - io_net.record_input_variables_paths(unknowns, ss, calibration=calibration, ignore_helpers=ignore_helpers) - ut_net = io_net.find_unknowns_targets_links(unknowns, targets, calibration=calibration, - ignore_helpers=ignore_helpers) + io_net.record_input_variables_paths(unknowns, ss) + ut_net = io_net.find_unknowns_targets_links(unknowns, targets) broken_unknowns = [] broken_targets = [] for u in unknowns: From 100b079ac802ae9a4f6ef01c9d879b06a2ccb970 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 6 May 2021 16:37:18 -0500 Subject: [PATCH 134/288] Fix bug with two_asset helper block implementation. Notes below: - Previously the vphi/wnkpc unknown/target pair were treated as non-excludable because the helper block they show up in, partial_ss_step2, had a non-helper block as a dependency or in other words depended on computed outputs from the main DAG. This rule of thumb helped us to separate the notion of a helper block that did not depend on computed outputs from the main DAG, i.e. partial_ss_step_1, and those that did depend on computed outputs from the main DAG. The issue was that the root-finding algorithm (broyden_custom) tried to form a Jacobian with respect to the unknowns/targets of the main DAG to update its guess for the unknowns, which included the unknown/target pair vphi/wnkpc. However, wnkpc was written in a way such that for *any* value of vphi it would be equal to 0. and hence the Jacobian was singular in that dimension. It is sensible that it was written this way since even though helper blocks may appear later on in the DAG/may be functions of computed outputs from the main DAG they still represent analytical solutions for an unknown given a target and that computed output. Hence, I have broadened the exclusion behavior of the _solve_for_unknowns_w_helper_blocks to treat all helper unknowns/targets as "excluded" in the strict sense that one should not have to *directly* numerically solve for them as with other unknowns/targets, but rather they may still need to be computed *indirectly* from other computed outputs. --- sequence_jacobian/steady_state/drivers.py | 38 +++++++++-------------- sequence_jacobian/steady_state/support.py | 8 ++--- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index bd56163..c65e340 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -75,19 +75,16 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, # Initial setup of blocks, targets, and dictionary of steady state values to be returned blocks_all = blocks + helper_blocks targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + + helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) helper_targets = {t: targets[t] for t in targets if t in helper_targets} + helper_outputs = {} ss_values = SteadyStateDict(calibration) ss_values.update(helper_targets) - helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - helper_indices = np.arange(len(blocks), len(blocks_all)) - helper_outputs = {} - if sort_blocks: - dep = graph.construct_dependency_graph(blocks, graph.construct_output_map(blocks, helper_blocks=helper_blocks), - calibration=ss_values, helper_blocks=helper_blocks) - topsorted = graph.topological_sort(dep) + topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) else: topsorted = range(len(blocks + helper_blocks)) @@ -110,7 +107,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c outputs = eval_block_ss(blocks_all[i], ss_values, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: - helper_outputs.update(outputs.toplevel) + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) ss_values.update(outputs) else: # Don't overwrite entries in ss_values corresponding to what has already @@ -123,8 +120,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c if helper_blocks: unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, - helper_targets, helper_indices, blocks_all, dep, - solver, solver_kwargs, + helper_targets, solver, solver_kwargs, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=ttol, verbose=verbose, fragile=fragile) @@ -154,8 +150,11 @@ def eval_block_ss(block, calibration, **kwargs): # Add the block's internal variables as inputs, if the block has an internal attribute input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - # If evaluating a CombinedBlock or SolvedBlock do not numerically solve for unknowns again just evaluate the - # block at the provided values in `calibration` + # If evaluating a SolvedBlock during a consistency check do not numerically solve for unknowns again, + # just evaluate the block at the provided values in `calibration`. + # This functionality is to invoke a true "PE"-like behavior for SolvedBlock, where unknowns are not re-solved for + # if they have already been found, which proves useful when SolvedBlocks' targets are dynamic restrictions + # but not static restrictions as is the case with dynamic debt rules in fiscal blocks. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} if "consistency_check" in kwargs and kwargs["consistency_check"] and "solver" in valid_input_kwargs: @@ -259,7 +258,7 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi elif solver == "solved": # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution # simply call residual_f once to populate the `ss_values` dict and verify if the targets are hit - if not np.all(np.isclose(residual_f(*unknowns.values()), 0.)): + if not np.all(np.isclose(residual_f(unknowns.values()), 0.)): raise RuntimeError("The `solver` kwarg was set to 'solved' even though the residual function indicates that" " the targets were not hit.") unknown_solutions = unknowns.values() @@ -270,7 +269,6 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, - helper_indices, blocks_all, block_dependencies, solver, solver_kwargs, constrained_method="linear_continuation", constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets @@ -282,22 +280,16 @@ def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unkn unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) - # Find the unknowns and targets that are both handled by helper blocks and are excludable from the main DAG - # evaluation by checking block dependencies - unknowns_excl, targets_excl = find_excludable_helper_blocks(blocks_all, block_dependencies, - helper_indices, helper_unknowns, helper_targets) - # Subset out the unknowns and targets that are not excludable from the main DAG loop - unknowns_non_excl = {k: unknowns[k] for k in misc.list_diff(list(unknowns.keys()), unknowns_excl)} - targets_non_excl = misc.dict_diff(targets, targets_excl) + unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) + targets_non_excl = misc.dict_diff(targets, helper_targets) # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and # we can omit them and their corresponding `unknowns` in the main DAG. - if np.all(np.isclose([targets_init_vals[t] for t in targets_excl.keys()], 0.)): + if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, solver, solver_kwargs, - residual_kwargs={"include_helpers": False}, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=tol, verbose=verbose, fragile=fragile) diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index 6860214..a7fbaad 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -108,16 +108,14 @@ def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): return unknowns_handled_by_helpers -def find_excludable_helper_blocks(blocks_all, block_dependencies, helper_indices, helper_unknowns, helper_targets): +def find_excludable_helper_blocks(blocks_all, helper_indices, helper_unknowns, helper_targets): """Of the set of helper_unknowns and helper_targets, find the ones that can be excluded from the main DAG for the purposes of numerically solving unknowns.""" excludable_helper_unknowns = {} excludable_helper_targets = {} for i in helper_indices: - # If the helper block has no dependencies on other blocks in the DAG - if not block_dependencies[i]: - excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) - excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) + excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) + excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) return excludable_helper_unknowns, excludable_helper_targets From 7c83702ce978c28b761ba5c866f14054c3b9c65c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 6 May 2021 17:13:40 -0500 Subject: [PATCH 135/288] Complete feature started in commit ab680e8 to allow top-level unknowns to supercede internal SolvedBlock unknowns --- sequence_jacobian/steady_state/drivers.py | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index c65e340..7545e37 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -100,12 +100,12 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c continue # Want to see hetoutputs elif hasattr(blocks_all[i], 'hetoutput') and blocks_all[i].hetoutput is not None: - outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, - consistency_check=consistency_check, verbose=verbose, **block_kwargs) + outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) else: - outputs = eval_block_ss(blocks_all[i], ss_values, consistency_check=consistency_check, - ttol=ttol, ctol=ctol, verbose=verbose, **block_kwargs) + outputs = eval_block_ss(blocks_all[i], ss_values, toplevel_unknowns=unknown_keys, + consistency_check=consistency_check, ttol=ttol, ctol=ctol, + verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) ss_values.update(outputs) @@ -145,19 +145,24 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c return ss_values -def eval_block_ss(block, calibration, **kwargs): +def eval_block_ss(block, calibration, toplevel_unknowns=None, consistency_check=False, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" + if toplevel_unknowns is None: + toplevel_unknowns = {} + # Add the block's internal variables as inputs, if the block has an internal attribute input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - # If evaluating a SolvedBlock during a consistency check do not numerically solve for unknowns again, - # just evaluate the block at the provided values in `calibration`. - # This functionality is to invoke a true "PE"-like behavior for SolvedBlock, where unknowns are not re-solved for - # if they have already been found, which proves useful when SolvedBlocks' targets are dynamic restrictions - # but not static restrictions as is the case with dynamic debt rules in fiscal blocks. + # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them + # at the provided set of unknowns if: + # A) SolvedBlock's internal DAG is subsumed by the main DAG, i.e. we want to solve for its unknown at the top-level. + # This is useful in steady state computations when SolvedBlocks' targets are only dynamic restrictions, as in + # the case of the debt adjustment fiscal rule + # B) A consistency check is being performed at a particular set of steady state values, so we don't need to + # re-solve for the unknowns of the the SolvedBlock valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if "consistency_check" in kwargs and kwargs["consistency_check"] and "solver" in valid_input_kwargs: + if "solver" in valid_input_kwargs and (set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) or consistency_check): input_kwarg_dict["solver"] = "solved" input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} From 9f30849882c0a4fa4977b4d2f4ae62a8154b9bfd Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 6 May 2021 17:41:32 -0500 Subject: [PATCH 136/288] Make residual() more compact by removing the HetBlock special case in the if statement --- sequence_jacobian/blocks/het_block.py | 2 +- sequence_jacobian/steady_state/drivers.py | 23 +++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index ed27602..18f80fe 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -239,7 +239,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} ss.update(aggregates) - if hetoutput: + if hetoutput and self.hetoutput is not None: hetoutputs = self.hetoutput.evaluate(ss) aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") else: diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 7545e37..ec4498b 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -98,21 +98,16 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c for i in topsorted: if not include_helpers and blocks_all[i] in helper_blocks: continue - # Want to see hetoutputs - elif hasattr(blocks_all[i], 'hetoutput') and blocks_all[i].hetoutput is not None: - outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, verbose=verbose, **block_kwargs) - ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) + outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, + consistency_check=consistency_check, ttol=ttol, ctol=ctol, + verbose=verbose, **block_kwargs) + if include_helpers and blocks_all[i] in helper_blocks: + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) + ss_values.update(outputs) else: - outputs = eval_block_ss(blocks_all[i], ss_values, toplevel_unknowns=unknown_keys, - consistency_check=consistency_check, ttol=ttol, ctol=ctol, - verbose=verbose, **block_kwargs) - if include_helpers and blocks_all[i] in helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) - ss_values.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) + # Don't overwrite entries in ss_values corresponding to what has already + # been solved for in helper_blocks so we can check for consistency after-the-fact + ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" From 3f2f1ee862ad80d325b57e0d3c199971b03a5acf Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 6 May 2021 17:53:48 -0500 Subject: [PATCH 137/288] Add back missing psip output --- sequence_jacobian/models/two_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py index d1e65aa..403bdbe 100644 --- a/sequence_jacobian/models/two_asset.py +++ b/sequence_jacobian/models/two_asset.py @@ -238,7 +238,7 @@ def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) I = K - (1 - delta) * K(-1) + k_adjust div = Y - w * N - I - psip - return I, div + return psip, I, div @simple From 07237bed2d6e99015f3db2766881e5cc135d0ca9 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 6 May 2021 20:21:04 -0500 Subject: [PATCH 138/288] Don't enforce that targets are hit for solver == "solved", since this functionality is mainly to allow SolvedBlocks to have PE-like steady_state evaluation behavior at a set of unknown values --- sequence_jacobian/steady_state/drivers.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index ec4498b..5cbc431 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -257,10 +257,8 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi unknown_solutions = list(unknown_solutions) elif solver == "solved": # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution - # simply call residual_f once to populate the `ss_values` dict and verify if the targets are hit - if not np.all(np.isclose(residual_f(unknowns.values()), 0.)): - raise RuntimeError("The `solver` kwarg was set to 'solved' even though the residual function indicates that" - " the targets were not hit.") + # simply call residual_f once to populate the `ss_values` dict + residual_f(unknowns.values()) unknown_solutions = unknowns.values() else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") From fb2cc10e13906bab93b24dfcfad10e73d50a1229 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 11 May 2021 13:22:43 -0500 Subject: [PATCH 139/288] Add dissolve kwarg to steady_state to require explicit requests by the user to dissolve a SolvedBlock --- sequence_jacobian/steady_state/drivers.py | 35 +++++++++++++++-------- sequence_jacobian/steady_state/support.py | 8 ++++-- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 5cbc431..00c994e 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -7,14 +7,14 @@ from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ - subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs, find_excludable_helper_blocks + subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs from .classes import SteadyStateDict from ..utilities import solvers, graph, misc # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, - helper_blocks=None, helper_targets=None, +def steady_state(blocks, calibration, unknowns, targets, dissolve=None, + sort_blocks=True, helper_blocks=None, helper_targets=None, consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): @@ -29,6 +29,9 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver targets: `dict` A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG + dissolve: `list` + A list of blocks, either SolvedBlock or CombinedBlock, where block-level unknowns are removed and subsumed + by the top-level unknowns, effectively removing the "solve" components of the blocks sort_blocks: `bool` Whether the blocks need to be topologically sorted (only False when this function is called from within a Block object, like CombinedBlock, that has already pre-sorted the blocks) @@ -60,7 +63,7 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, constrained_method: `str` When using solvers that typically only take an initial value, x0, we provide a few options for manipulating the solver to account for bounds when finding a solution. These methods are described in the - constrained_multivariate_residual function. + constrained_multivariate_residual function constrained_kwargs: The keyword arguments that the user's chosen constrained method requires to run @@ -68,9 +71,9 @@ def steady_state(blocks, calibration, unknowns, targets, sort_blocks=True, A dictionary containing all of the pre-specified values and computed values from the steady state computation """ - helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ - instantiate_steady_state_mutable_kwargs(helper_blocks, helper_targets, block_kwargs, - solver_kwargs, constrained_kwargs) + dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ + instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, + block_kwargs, solver_kwargs, constrained_kwargs) # Initial setup of blocks, targets, and dictionary of steady state values to be returned blocks_all = blocks + helper_blocks @@ -99,7 +102,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c if not include_helpers and blocks_all[i] in helper_blocks: continue outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, - consistency_check=consistency_check, ttol=ttol, ctol=ctol, + dissolve=dissolve, consistency_check=consistency_check, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) @@ -140,10 +143,11 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c return ss_values -def eval_block_ss(block, calibration, toplevel_unknowns=None, consistency_check=False, **kwargs): +def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, consistency_check=False, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" if toplevel_unknowns is None: toplevel_unknowns = {} + block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False # Add the block's internal variables as inputs, if the block has an internal attribute input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel @@ -157,9 +161,16 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, consistency_check= # re-solve for the unknowns of the the SolvedBlock valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if "solver" in valid_input_kwargs and (set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) or consistency_check): - input_kwarg_dict["solver"] = "solved" - input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} + if block.name in dissolve: + if "solver" in valid_input_kwargs and (block_unknowns_in_toplevel_unknowns or consistency_check): + input_kwarg_dict["solver"] = "solved" + input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} + elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: + raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," + f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," + f" {set(toplevel_unknowns)}.\n" + f"If the user provides a set of top-level unknowns that subsume block-level unknowns," + f" it must be explicitly declared in `dissolve`.") return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py index a7fbaad..88374b6 100644 --- a/sequence_jacobian/steady_state/support.py +++ b/sequence_jacobian/steady_state/support.py @@ -5,9 +5,11 @@ import numpy as np -def instantiate_steady_state_mutable_kwargs(helper_blocks, helper_targets, block_kwargs, solver_kwargs, - constrained_kwargs): +def instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, + block_kwargs, solver_kwargs, constrained_kwargs): """Instantiate mutable types from `None` default values in the steady_state function""" + if dissolve is None: + dissolve = [] if helper_blocks is None and helper_targets is None: helper_blocks = [] helper_targets = [] @@ -24,7 +26,7 @@ def instantiate_steady_state_mutable_kwargs(helper_blocks, helper_targets, block if constrained_kwargs is None: constrained_kwargs = {} - return helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs + return dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs def provide_solver_default(unknowns): From 3f603f5b76f7afc76f2dbf2ccec90a206af63766 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 11 May 2021 13:40:13 -0500 Subject: [PATCH 140/288] Change ~ to > and set compatibility w/ resp. to minor versions in requirements.txt --- requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 55c1367..3c3e53f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -numpy~=1.18.5 -scipy~=1.5.0 -numba~=0.50.1 -xarray~=0.17.0 +numpy>=1.18 +scipy>=1.5 +numba>=0.50 +xarray>=0.17 -pytest~=5.4.3 -setuptools~=49.2.0 \ No newline at end of file +pytest>=5.4 +setuptools>=49.2 \ No newline at end of file From ca989b1946f4bf30369eeda144b2c2b547accefc Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 19 May 2021 09:34:44 -0500 Subject: [PATCH 141/288] Initialize J after checking whether y satisfies the tol in broyden_custom to avoid inaccurate ss_value caching --- sequence_jacobian/utilities/solvers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sequence_jacobian/utilities/solvers.py b/sequence_jacobian/utilities/solvers.py index 24bea55..076a809 100644 --- a/sequence_jacobian/utilities/solvers.py +++ b/sequence_jacobian/utilities/solvers.py @@ -76,8 +76,6 @@ def broyden_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verb if y is None: y = f(x) - # initialize J with Newton! - J = obtain_J(f, x, y) for count in range(maxcount): if verbose: printit(count, x, y) @@ -85,6 +83,10 @@ def broyden_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verb if np.max(np.abs(y)) < tol: return x, y + # initialize J with Newton! + if count == 0: + J = obtain_J(f, x, y) + if len(x) == len(y): dx = np.linalg.solve(J, -y) elif len(x) < len(y): From adfb504895a312f3f703b2f2ed5910216e4f5ae2 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 20 May 2021 16:04:46 -0500 Subject: [PATCH 142/288] Update signature of block methods. --- sequence_jacobian/blocks/simple_block.py | 12 +++++++----- sequence_jacobian/primitives.py | 9 +++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index fa735ae..d674265 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -64,7 +64,8 @@ def jac(self, ss, T=None, shock_list=None): def steady_state(self, calibration): input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} - output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [misc.numeric_primitive(self.f(**input_args))] + output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ + misc.numeric_primitive(self.f(**input_args))] return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) def impulse_nonlinear(self, ss, exogenous): @@ -90,18 +91,19 @@ def jacobian(self, ss, exogenous=None, T=None, Js=None): ---------- ss : dict, steady state values + exogenous : list of str, optional + names of input variables to differentiate wrt; if omitted, assume all inputs T : int, optional number of time periods for explicit T*T Jacobian if omitted, more efficient SimpleSparse objects returned - exogenous : list of str, optional - names of input variables to differentiate wrt; if omitted, assume all inputs + Js : dict of {str: JacobianDict}, optional + pre-computed Jacobians Returns ------- J : dict of {str: dict of {str: array(T,T)}} J[o][i] for output o and input i gives Jacobian of o with respect to i - This Jacobian is a SimpleSparse object or, if T specific, a T*T matrix, omitted by convention - if zero + This Jacobian is a SimpleSparse object or, if T specific, a T*T matrix, omitted by convention if zero """ if exogenous is None: diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index f911589..375f57b 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -9,6 +9,7 @@ from .steady_state.support import provide_solver_default from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G +from .steady_state.classes import SteadyStateDict from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict @@ -70,15 +71,15 @@ def outputs(self): # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical # input and output argument structure def steady_state(self, calibration: Dict[str, Union[Real, Array]], - **kwargs) -> Dict[str, Union[Real, Array]]: + **kwargs) -> SteadyStateDict: raise NotImplementedError(f'{type(self)} does not implement .steady_state()') def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: raise NotImplementedError(f'{type(self)} does not implement .impulse_nonlinear()') def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: raise NotImplementedError(f'{type(self)} does not implement .impulse_linear()') def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = None, @@ -88,7 +89,7 @@ def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = Non def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], - solver: Optional[str] = "", **kwargs) -> Dict[str, Union[Real, Array]]: + solver: Optional[str] = "", **kwargs) -> SteadyStateDict: """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" From 0ca40511e740bddd5204a7473e6674c81a44a6e9 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 20 May 2021 16:10:11 -0500 Subject: [PATCH 143/288] Relax behavior of dissolve, so that it works with fixed parameters (not internally calibrated). --- sequence_jacobian/steady_state/drivers.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 00c994e..96aa5ce 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -102,8 +102,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c if not include_helpers and blocks_all[i] in helper_blocks: continue outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, - dissolve=dissolve, consistency_check=consistency_check, - verbose=verbose, **block_kwargs) + dissolve=dissolve, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) ss_values.update(outputs) @@ -143,7 +142,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c return ss_values -def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, consistency_check=False, **kwargs): +def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" if toplevel_unknowns is None: toplevel_unknowns = {} @@ -153,18 +152,12 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, con input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them - # at the provided set of unknowns if: - # A) SolvedBlock's internal DAG is subsumed by the main DAG, i.e. we want to solve for its unknown at the top-level. - # This is useful in steady state computations when SolvedBlocks' targets are only dynamic restrictions, as in - # the case of the debt adjustment fiscal rule - # B) A consistency check is being performed at a particular set of steady state values, so we don't need to - # re-solve for the unknowns of the the SolvedBlock + # at the provided set of unknowns if included in dissolve. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} if block.name in dissolve: - if "solver" in valid_input_kwargs and (block_unknowns_in_toplevel_unknowns or consistency_check): - input_kwarg_dict["solver"] = "solved" - input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} + input_kwarg_dict["solver"] = "solved" + input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," From 63494250afe1e621a2515b6762568184a77ba156 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 20 May 2021 16:37:02 -0500 Subject: [PATCH 144/288] Check that block to be dissolved is indeed a SolvedBlock. --- sequence_jacobian/steady_state/drivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 96aa5ce..6bfe552 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -155,7 +155,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k # at the provided set of unknowns if included in dissolve. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if block.name in dissolve: + if block.name in dissolve and "solver" in valid_input_kwargs: input_kwarg_dict["solver"] = "solved" input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: From 5aac4b23a39ac9eab242246f86c5cf49775a558d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 20 May 2021 16:40:24 -0500 Subject: [PATCH 145/288] For consistency, `dissolve=` takes list of blocks not list of strings. --- sequence_jacobian/steady_state/drivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py index 6bfe552..dbe6e9c 100644 --- a/sequence_jacobian/steady_state/drivers.py +++ b/sequence_jacobian/steady_state/drivers.py @@ -155,7 +155,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k # at the provided set of unknowns if included in dissolve. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if block.name in dissolve and "solver" in valid_input_kwargs: + if block in dissolve and "solver" in valid_input_kwargs: input_kwarg_dict["solver"] = "solved" input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: From be5584e03b686360abcbec90d7b49aa00eb8ab0b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 24 May 2021 09:55:14 -0500 Subject: [PATCH 146/288] Exclude tests from top-level packages installed by pip to site-packages --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c1f64c3..65e3369 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from pathlib import Path -from setuptools import find_packages, setup +from setuptools import setup, find_packages with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() @@ -12,7 +12,6 @@ setup( name="sequence-jacobian", - packages=find_packages(), python_requires=">=3.7", install_requires=read("requirements.txt").splitlines(), version="0.0.1", @@ -27,4 +26,7 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], + + packages=find_packages(where='sequence_jacobian'), + package_dir={'': 'sequence_jacobian'}, ) From cdcf35cefa570ec1ae05c4d58ead3263c407e4da Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 24 May 2021 10:30:41 -0500 Subject: [PATCH 147/288] Move sequence_jacobian package into src per packaging best practices --- setup.py | 4 ++-- .../sequence_jacobian}/__init__.py | 1 - .../sequence_jacobian}/blocks/__init__.py | 0 .../blocks/auxiliary_blocks/__init__.py | 0 .../auxiliary_blocks/jacobiandict_block.py | 0 .../blocks/combined_block.py | 0 .../sequence_jacobian}/blocks/het_block.py | 20 +++++++++---------- .../sequence_jacobian}/blocks/simple_block.py | 4 ++-- .../sequence_jacobian}/blocks/solved_block.py | 0 .../blocks/support/__init__.py | 0 .../blocks/support/impulse.py | 0 .../blocks/support/simple_displacement.py | 0 .../sequence_jacobian}/devtools/__init__.py | 0 .../sequence_jacobian}/devtools/analysis.py | 0 .../sequence_jacobian}/devtools/debug.py | 0 .../sequence_jacobian}/devtools/deprecate.py | 0 .../sequence_jacobian}/devtools/upgrade.py | 0 .../sequence_jacobian}/estimation.py | 2 +- .../sequence_jacobian}/jacobian/__init__.py | 0 .../sequence_jacobian}/jacobian/classes.py | 0 .../sequence_jacobian}/jacobian/drivers.py | 2 +- .../sequence_jacobian}/jacobian/support.py | 0 .../sequence_jacobian}/models/__init__.py | 0 .../sequence_jacobian}/models/hank.py | 4 ++-- .../models/krusell_smith.py | 0 .../sequence_jacobian}/models/rbc.py | 1 - .../sequence_jacobian}/models/two_asset.py | 0 .../sequence_jacobian}/nonlinear.py | 4 ++-- .../sequence_jacobian}/primitives.py | 0 .../steady_state/__init__.py | 0 .../steady_state/classes.py | 0 .../steady_state/drivers.py | 0 .../steady_state/support.py | 0 .../sequence_jacobian}/utilities/__init__.py | 0 .../utilities/differentiate.py | 0 .../utilities/discretize.py | 0 .../utilities/forward_step.py | 2 -- .../sequence_jacobian}/utilities/graph.py | 2 -- .../utilities/interpolate.py | 0 .../sequence_jacobian}/utilities/misc.py | 0 .../utilities/optimized_routines.py | 2 -- .../sequence_jacobian}/utilities/solvers.py | 1 - .../visualization/__init__.py | 0 .../visualization/draw_dag.py | 0 44 files changed, 20 insertions(+), 29 deletions(-) rename {sequence_jacobian => src/sequence_jacobian}/__init__.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/auxiliary_blocks/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/auxiliary_blocks/jacobiandict_block.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/combined_block.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/het_block.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/simple_block.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/solved_block.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/support/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/support/impulse.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/blocks/support/simple_displacement.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/devtools/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/devtools/analysis.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/devtools/debug.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/devtools/deprecate.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/devtools/upgrade.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/estimation.py (98%) rename {sequence_jacobian => src/sequence_jacobian}/jacobian/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/jacobian/classes.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/jacobian/drivers.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/jacobian/support.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/models/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/models/hank.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/models/krusell_smith.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/models/rbc.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/models/two_asset.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/nonlinear.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/primitives.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/steady_state/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/steady_state/classes.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/steady_state/drivers.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/steady_state/support.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/differentiate.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/discretize.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/forward_step.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/graph.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/interpolate.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/misc.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/optimized_routines.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/utilities/solvers.py (99%) rename {sequence_jacobian => src/sequence_jacobian}/visualization/__init__.py (100%) rename {sequence_jacobian => src/sequence_jacobian}/visualization/draw_dag.py (100%) diff --git a/setup.py b/setup.py index 65e3369..2e69c24 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,6 @@ "Operating System :: OS Independent", ], - packages=find_packages(where='sequence_jacobian'), - package_dir={'': 'sequence_jacobian'}, + packages=find_packages(where='src'), + package_dir={'': 'src'}, ) diff --git a/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py similarity index 99% rename from sequence_jacobian/__init__.py rename to src/sequence_jacobian/__init__.py index 2f7abb6..b09c1f0 100644 --- a/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -28,4 +28,3 @@ formatwarning_orig = warnings.formatwarning warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ formatwarning_orig(message, category, filename, lineno, line='') - diff --git a/sequence_jacobian/blocks/__init__.py b/src/sequence_jacobian/blocks/__init__.py similarity index 100% rename from sequence_jacobian/blocks/__init__.py rename to src/sequence_jacobian/blocks/__init__.py diff --git a/sequence_jacobian/blocks/auxiliary_blocks/__init__.py b/src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py similarity index 100% rename from sequence_jacobian/blocks/auxiliary_blocks/__init__.py rename to src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py diff --git a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py similarity index 100% rename from sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py rename to src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py diff --git a/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py similarity index 100% rename from sequence_jacobian/blocks/combined_block.py rename to src/sequence_jacobian/blocks/combined_block.py diff --git a/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py similarity index 99% rename from sequence_jacobian/blocks/het_block.py rename to src/sequence_jacobian/blocks/het_block.py index 18f80fe..ea6f2db 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -113,7 +113,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # Checking that the various inputs/outputs attributes are correctly set if self.exogenous + '_p' not in self.back_step_inputs: raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") - + for pol in self.policy: if pol not in self.back_step_outputs: raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") @@ -255,7 +255,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal kwargs are constant at their ss values. Analog to SimpleBlock.td. CANNOT provide time-varying paths of grid or Markov transition matrix for now. - + Parameters ---------- ss : dict @@ -557,7 +557,7 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) else: raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') - + # want to record inputs in ssin, but remove _p, add in hetinput inputs if there for k in self.inputs_to_be_primed: ssin[k] = ssin[k + '_p'] @@ -598,7 +598,7 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see if D_seed is None: # compute stationary distribution for exogenous variable pi = utils.discretize.stationary(Pi, pi_seed) - + # now initialize full distribution with this, assuming uniform distribution on endogenous vars endogenous_dims = [grid[k].shape[0] for k in self.policy] D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) @@ -623,7 +623,7 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see D = Dnew else: raise ValueError(f'No convergence after {maxit} forward iterations!') - + return D '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper @@ -633,7 +633,7 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see - Step 4: J_from_F to get Jacobian from fake news matrix ''' - def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, + def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): # shock perturbs outputs shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, @@ -659,7 +659,7 @@ def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, return curlyV, curlyD, curlyY - def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, + def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): """Iterate policy steps backward T times for a single shock.""" # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None @@ -676,7 +676,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, sso din_dict = {input_shocked: 1} # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) # infer dimensions from this and initialize empty arrays @@ -691,7 +691,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, sso # fill in anticipation effects for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, ssin_dict, ssout_list, + output_list, ssin_dict, ssout_list, Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] @@ -700,7 +700,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, sso def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. - + Note we depart from definition in paper by applying the demeaning operator in addition to Lambda at each step. This does not affect products with curlyD (which are the only way curlyPs enter Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits diff --git a/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py similarity index 99% rename from sequence_jacobian/blocks/simple_block.py rename to src/sequence_jacobian/blocks/simple_block.py index d674265..779f27f 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -20,7 +20,7 @@ def simple(f): class SimpleBlock(Block): """Generated from simple block written in Dynare-ish style and decorated with @simple, e.g. - + @simple def production(Z, K, L, alpha): Y = Z * K(-1) ** alpha * L ** (1 - alpha) @@ -29,7 +29,7 @@ def production(Z, K, L, alpha): which is a SimpleBlock that takes in Z, K, L, and alpha, all of which can be either constants or series, and implements a Cobb-Douglas production function, noting that for production today we use the capital K(-1) determined yesterday. - + Key methods are .ss, .td, and .jac, like HetBlock. """ diff --git a/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py similarity index 100% rename from sequence_jacobian/blocks/solved_block.py rename to src/sequence_jacobian/blocks/solved_block.py diff --git a/sequence_jacobian/blocks/support/__init__.py b/src/sequence_jacobian/blocks/support/__init__.py similarity index 100% rename from sequence_jacobian/blocks/support/__init__.py rename to src/sequence_jacobian/blocks/support/__init__.py diff --git a/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py similarity index 100% rename from sequence_jacobian/blocks/support/impulse.py rename to src/sequence_jacobian/blocks/support/impulse.py diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/src/sequence_jacobian/blocks/support/simple_displacement.py similarity index 100% rename from sequence_jacobian/blocks/support/simple_displacement.py rename to src/sequence_jacobian/blocks/support/simple_displacement.py diff --git a/sequence_jacobian/devtools/__init__.py b/src/sequence_jacobian/devtools/__init__.py similarity index 100% rename from sequence_jacobian/devtools/__init__.py rename to src/sequence_jacobian/devtools/__init__.py diff --git a/sequence_jacobian/devtools/analysis.py b/src/sequence_jacobian/devtools/analysis.py similarity index 100% rename from sequence_jacobian/devtools/analysis.py rename to src/sequence_jacobian/devtools/analysis.py diff --git a/sequence_jacobian/devtools/debug.py b/src/sequence_jacobian/devtools/debug.py similarity index 100% rename from sequence_jacobian/devtools/debug.py rename to src/sequence_jacobian/devtools/debug.py diff --git a/sequence_jacobian/devtools/deprecate.py b/src/sequence_jacobian/devtools/deprecate.py similarity index 100% rename from sequence_jacobian/devtools/deprecate.py rename to src/sequence_jacobian/devtools/deprecate.py diff --git a/sequence_jacobian/devtools/upgrade.py b/src/sequence_jacobian/devtools/upgrade.py similarity index 100% rename from sequence_jacobian/devtools/upgrade.py rename to src/sequence_jacobian/devtools/upgrade.py diff --git a/sequence_jacobian/estimation.py b/src/sequence_jacobian/estimation.py similarity index 98% rename from sequence_jacobian/estimation.py rename to src/sequence_jacobian/estimation.py index a633640..8becc42 100644 --- a/sequence_jacobian/estimation.py +++ b/src/sequence_jacobian/estimation.py @@ -14,7 +14,7 @@ def all_covariances(M, sigmas): Parameters ---------- - M : array (T*O*Z), stacked impulse responses of nO variables to nZ shocks (MA(T-1) representation) + M : array (T*O*Z), stacked impulse responses of nO variables to nZ shocks (MA(T-1) representation) sigmas : array (Z), standard deviations of shocks Returns diff --git a/sequence_jacobian/jacobian/__init__.py b/src/sequence_jacobian/jacobian/__init__.py similarity index 100% rename from sequence_jacobian/jacobian/__init__.py rename to src/sequence_jacobian/jacobian/__init__.py diff --git a/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py similarity index 100% rename from sequence_jacobian/jacobian/classes.py rename to src/sequence_jacobian/jacobian/classes.py diff --git a/sequence_jacobian/jacobian/drivers.py b/src/sequence_jacobian/jacobian/drivers.py similarity index 99% rename from sequence_jacobian/jacobian/drivers.py rename to src/sequence_jacobian/jacobian/drivers.py index 830554b..16131f9 100644 --- a/sequence_jacobian/jacobian/drivers.py +++ b/src/sequence_jacobian/jacobian/drivers.py @@ -6,7 +6,7 @@ from .support import pack_vectors, unpack_vectors from ..utilities import misc, graph -'''Drivers: +'''Drivers: - get_H_U : get H_U matrix mapping all unknowns to all targets - get_impulse : get single GE impulse response - get_G : get G matrices characterizing all GE impulse responses diff --git a/sequence_jacobian/jacobian/support.py b/src/sequence_jacobian/jacobian/support.py similarity index 100% rename from sequence_jacobian/jacobian/support.py rename to src/sequence_jacobian/jacobian/support.py diff --git a/sequence_jacobian/models/__init__.py b/src/sequence_jacobian/models/__init__.py similarity index 100% rename from sequence_jacobian/models/__init__.py rename to src/sequence_jacobian/models/__init__.py diff --git a/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py similarity index 99% rename from sequence_jacobian/models/hank.py rename to src/sequence_jacobian/models/hank.py index 8c2ce79..8ee9576 100644 --- a/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -210,11 +210,11 @@ def res(x): # extra evaluation for reporting ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi) - + # check Walras's law goods_mkt = 1 - ss['C'] assert np.abs(goods_mkt) < 1E-8 - + # add aggregate variables ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, diff --git a/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py similarity index 100% rename from sequence_jacobian/models/krusell_smith.py rename to src/sequence_jacobian/models/krusell_smith.py diff --git a/sequence_jacobian/models/rbc.py b/src/sequence_jacobian/models/rbc.py similarity index 99% rename from sequence_jacobian/models/rbc.py rename to src/sequence_jacobian/models/rbc.py index 61db8b0..3e44096 100644 --- a/sequence_jacobian/models/rbc.py +++ b/src/sequence_jacobian/models/rbc.py @@ -92,4 +92,3 @@ def rbc_ss(r=0.01, eis=1, frisch=1, delta=0.025, alpha=0.11): return {'beta': beta, 'eis': eis, 'frisch': frisch, 'vphi': vphi, 'delta': delta, 'alpha': alpha, 'Z': Z, 'K': K, 'I': I, 'Y': Y, 'L': 1, 'C': C, 'w': w, 'r': r, 'walras': walras, 'euler': euler, 'goods_mkt': goods_mkt} - diff --git a/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py similarity index 100% rename from sequence_jacobian/models/two_asset.py rename to src/sequence_jacobian/models/two_asset.py diff --git a/sequence_jacobian/nonlinear.py b/src/sequence_jacobian/nonlinear.py similarity index 99% rename from sequence_jacobian/nonlinear.py rename to src/sequence_jacobian/nonlinear.py index 2d9bc97..da33ee8 100644 --- a/sequence_jacobian/nonlinear.py +++ b/src/sequence_jacobian/nonlinear.py @@ -12,7 +12,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=Fa """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. - + Parameters ---------- block_list : list, blocks in model (SimpleBlocks or HetBlocks) @@ -81,7 +81,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=Fa def td_map(block_list, ss, exogenous, unknowns=None, sort=None, monotonic=False, returnindividual=False, grid_paths=None): """Helper for td_solve, calculates H(U, Z), where U and Z are in kwargs. - + Goes through block_list, topologically sorts the implied DAG, calculates H(U, Z), with missing paths always being interpreted as remaining at the steady state for a particular variable""" diff --git a/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py similarity index 100% rename from sequence_jacobian/primitives.py rename to src/sequence_jacobian/primitives.py diff --git a/sequence_jacobian/steady_state/__init__.py b/src/sequence_jacobian/steady_state/__init__.py similarity index 100% rename from sequence_jacobian/steady_state/__init__.py rename to src/sequence_jacobian/steady_state/__init__.py diff --git a/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py similarity index 100% rename from sequence_jacobian/steady_state/classes.py rename to src/sequence_jacobian/steady_state/classes.py diff --git a/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py similarity index 100% rename from sequence_jacobian/steady_state/drivers.py rename to src/sequence_jacobian/steady_state/drivers.py diff --git a/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py similarity index 100% rename from sequence_jacobian/steady_state/support.py rename to src/sequence_jacobian/steady_state/support.py diff --git a/sequence_jacobian/utilities/__init__.py b/src/sequence_jacobian/utilities/__init__.py similarity index 100% rename from sequence_jacobian/utilities/__init__.py rename to src/sequence_jacobian/utilities/__init__.py diff --git a/sequence_jacobian/utilities/differentiate.py b/src/sequence_jacobian/utilities/differentiate.py similarity index 100% rename from sequence_jacobian/utilities/differentiate.py rename to src/sequence_jacobian/utilities/differentiate.py diff --git a/sequence_jacobian/utilities/discretize.py b/src/sequence_jacobian/utilities/discretize.py similarity index 100% rename from sequence_jacobian/utilities/discretize.py rename to src/sequence_jacobian/utilities/discretize.py diff --git a/sequence_jacobian/utilities/forward_step.py b/src/sequence_jacobian/utilities/forward_step.py similarity index 99% rename from sequence_jacobian/utilities/forward_step.py rename to src/sequence_jacobian/utilities/forward_step.py index 96ffa3b..a80b7bf 100644 --- a/sequence_jacobian/utilities/forward_step.py +++ b/src/sequence_jacobian/utilities/forward_step.py @@ -171,5 +171,3 @@ def forward_step_transpose_endo_2d(D, x_i, y_i, x_pi, y_pi): (1-alpha) * beta * D[iz, ixp+1, iyp] + (1-alpha) * (1-beta) * D[iz, ixp+1, iyp+1]) return Dnew - - diff --git a/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py similarity index 99% rename from sequence_jacobian/utilities/graph.py rename to src/sequence_jacobian/utilities/graph.py index aa01ecd..a22de24 100644 --- a/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -277,5 +277,3 @@ def __getitem__(self, i): def __repr__(self): return self.mylist.__repr__() - - diff --git a/sequence_jacobian/utilities/interpolate.py b/src/sequence_jacobian/utilities/interpolate.py similarity index 100% rename from sequence_jacobian/utilities/interpolate.py rename to src/sequence_jacobian/utilities/interpolate.py diff --git a/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py similarity index 100% rename from sequence_jacobian/utilities/misc.py rename to src/sequence_jacobian/utilities/misc.py diff --git a/sequence_jacobian/utilities/optimized_routines.py b/src/sequence_jacobian/utilities/optimized_routines.py similarity index 99% rename from sequence_jacobian/utilities/optimized_routines.py rename to src/sequence_jacobian/utilities/optimized_routines.py index 94f1724..e0499e3 100644 --- a/sequence_jacobian/utilities/optimized_routines.py +++ b/src/sequence_jacobian/utilities/optimized_routines.py @@ -41,5 +41,3 @@ def fast_aggregate(X, Y): for t in range(T): Z[t] = Xnew[t, :] @ Ynew[t, :] return Z - - diff --git a/sequence_jacobian/utilities/solvers.py b/src/sequence_jacobian/utilities/solvers.py similarity index 99% rename from sequence_jacobian/utilities/solvers.py rename to src/sequence_jacobian/utilities/solvers.py index 076a809..d29c081 100644 --- a/sequence_jacobian/utilities/solvers.py +++ b/src/sequence_jacobian/utilities/solvers.py @@ -145,4 +145,3 @@ def printit(it, x, y, **kwargs): for kw, val in kwargs.items(): print(f'{kw} = {val:.3f}') print('\n') - diff --git a/sequence_jacobian/visualization/__init__.py b/src/sequence_jacobian/visualization/__init__.py similarity index 100% rename from sequence_jacobian/visualization/__init__.py rename to src/sequence_jacobian/visualization/__init__.py diff --git a/sequence_jacobian/visualization/draw_dag.py b/src/sequence_jacobian/visualization/draw_dag.py similarity index 100% rename from sequence_jacobian/visualization/draw_dag.py rename to src/sequence_jacobian/visualization/draw_dag.py From 62cfa5303090c58a5b510558d5e69fa9f3b64537 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 24 May 2021 10:50:09 -0500 Subject: [PATCH 148/288] Remove pytest and setuptools dependencies --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3c3e53f..48faf2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,4 @@ numpy>=1.18 scipy>=1.5 numba>=0.50 -xarray>=0.17 - -pytest>=5.4 -setuptools>=49.2 \ No newline at end of file +xarray>=0.17 \ No newline at end of file From 546354004bbc4ab8b12b239cb6a3793bcb4f8304 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 14:25:41 -0500 Subject: [PATCH 149/288] Arithmetic operations (+, -, *, /) for ImpulseDicts. Scope: numbers and SteadyStateDicts. --- sequence_jacobian/blocks/support/impulse.py | 100 ++++++++++++-------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index 4b75584..907819e 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -6,16 +6,13 @@ class ImpulseDict: - def __init__(self, impulse, ss): + def __init__(self, impulse): if isinstance(impulse, ImpulseDict): self.impulse = impulse.impulse - self.ss = impulse.ss else: - if not isinstance(impulse, dict) or not isinstance(ss, SteadyStateDict): - raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses' - ' and a `SteadyStateDict` of steady state values.') + if not isinstance(impulse, dict): + raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses.') self.impulse = impulse - self.ss = ss def __repr__(self): return f'' @@ -23,47 +20,70 @@ def __repr__(self): def __iter__(self): return iter(self.impulse.items()) - def __mul__(self, x): - return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) - - def __rmul__(self, x): - return type(self)({k: x * v for k, v in self.impulse.items()}, self.ss) - def __or__(self, other): if not isinstance(other, ImpulseDict): raise ValueError('Trying to merge an ImpulseDict with something else.') - if self.ss != other.ss: - raise ValueError('Trying to merge ImpulseDicts with different steady states.') - # make a copy, then add additional impulses - merged = type(self)(self.impulse, self.ss) + # Union returns a new ImpulseDict + merged = type(self)(self.impulse) merged.impulse.update(other.impulse) return merged - def __getitem__(self, x): + def __getitem__(self, item): # Behavior similar to pandas - if isinstance(x, str): - # case 1: ImpulseDict['C'] returns array - return self.impulse[x] - if isinstance(x, list): - # case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts - return type(self)({k: self.impulse[k] for k in x}, self.ss) + if isinstance(item, str): + # Case 1: ImpulseDict['C'] returns array + return self.impulse[item] + if isinstance(item, list): + # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts + return type(self)({k: self.impulse[k] for k in item}) - def normalize(self, x=None): - if x is None: - # default: normalize by steady state if not zero - impulse = {k: v/self.ss[k] if not np.isclose(self.ss[k], 0) else v for k, v in self.impulse.items()} - else: - # normalize by steady state of x - if x not in self.ss.keys(): - raise ValueError(f'Cannot normalize with {x}: steady state is unknown.') - elif np.isclose(self.ss[x], 0): - raise ValueError(f'Cannot normalize with {x}: steady state is zero.') - else: - impulse = {k: v / self.ss[x] for k, v in self.impulse.items()} - return type(self)(impulse, self.ss) + def __add__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v + other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v + other[k] for k, v in self.impulse.items()}) + + def __sub__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v - other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v - other[k] for k, v in self.impulse.items()}) + + def __mul__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v * other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + + def __rmul__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v * other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v * other[k] for k, v in self.impulse.items()}) - def levels(self): - return type(self)({k: v + self.ss[k] for k, v in self.impulse.items()}, self.ss) + def __truediv__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v / other for k, v in self.impulse.items()}) + # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero + if isinstance(other, SteadyStateDict): + return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) - def deviations(self): - return type(self)({k: v - self.ss[k] for k, v in self.impulse.items()}, self.ss) + # def normalize(self, x=None): + # if x is None: + # # default: normalize by steady state if not zero + # impulse = {k: v/self.ss[k] if not np.isclose(self.ss[k], 0) else v for k, v in self.impulse.items()} + # else: + # # normalize by steady state of x + # if x not in self.ss.keys(): + # raise ValueError(f'Cannot normalize with {x}: steady state is unknown.') + # elif np.isclose(self.ss[x], 0): + # raise ValueError(f'Cannot normalize with {x}: steady state is zero.') + # else: + # impulse = {k: v / self.ss[x] for k, v in self.impulse.items()} + # return type(self)(impulse, self.ss) + # + # def levels(self): + # return type(self)({k: v + self.ss[k] for k, v in self.impulse.items()}, self.ss) + # + # def deviations(self): + # return type(self)({k: v - self.ss[k] for k, v in self.impulse.items()}, self.ss) From 77474884b2d14322c0d1a7b09668132863def46b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 14:35:59 -0500 Subject: [PATCH 150/288] Drivers now use arithmetic operations (+, -, *, /) of ImpulseDicts. --- sequence_jacobian/blocks/combined_block.py | 4 ++-- sequence_jacobian/blocks/het_block.py | 6 +++--- sequence_jacobian/blocks/simple_block.py | 4 ++-- sequence_jacobian/primitives.py | 4 ++-- tests/base/test_public_classes.py | 9 ++------- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 46fb395..9658c47 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -78,7 +78,7 @@ def impulse_nonlinear(self, ss, exogenous, **kwargs): if input_args: # If this block is actually perturbed irf_nonlin_partial_eq.update({k: v - ss[k] for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) - return ImpulseDict(irf_nonlin_partial_eq, ss).levels() + return ImpulseDict(irf_nonlin_partial_eq) + ss def impulse_linear(self, ss, exogenous, T=None, Js=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from @@ -90,7 +90,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None): if input_args: # If this block is actually perturbed irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T, Js=Js)}) - return ImpulseDict(irf_lin_partial_eq, ss) + return ImpulseDict(irf_lin_partial_eq) def jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index 18f80fe..a4b0c55 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -356,9 +356,9 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal # return either this, or also include distributional information if returnindividual: return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, - 'D': D_path}, ss) + 'D': D_path}) else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs}, ss) + return ImpulseDict({**aggregates, **aggregate_hetoutputs}) def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): # infer T from exogenous, check that all shocks have same length @@ -367,7 +367,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): raise ValueError('Not all shocks in kwargs (exogenous) are same length!') T = shock_lengths[0] - return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous), ss) + return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index d674265..a0689fa 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -79,10 +79,10 @@ def impulse_nonlinear(self, ss, exogenous): if k not in input_args: input_args[k] = ignore(ss[k]) - return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list), ss) + return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) def impulse_linear(self, ss, exogenous, T=None, Js=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous), ss) + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) def jacobian(self, ss, exogenous=None, T=None, Js=None): """Assemble nested dict of Jacobians diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 375f57b..6097345 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -109,7 +109,7 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], irf_nonlin_gen_eq = td_solve(blocks, ss, exogenous={k: ss[k] + v for k, v in exogenous.items()}, unknowns=unknowns, targets=targets, Js=Js, **kwargs) - return ImpulseDict(irf_nonlin_gen_eq, ss) + return ImpulseDict(irf_nonlin_gen_eq) def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], @@ -122,7 +122,7 @@ def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) - return ImpulseDict(irf_lin_gen_eq, ss) + return ImpulseDict(irf_lin_gen_eq) def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str], diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index e37eea6..9af4a81 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -58,7 +58,6 @@ def test_impulsedict(krusell_smith_dag): # Linearized impulse responses as deviations, nonlinear as levels ir_lin = ks_model.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) - ir_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.01 * 0.5 ** np.arange(T)}, unknowns, targets) # Get method assert isinstance(ir_lin, ImpulseDict) @@ -69,11 +68,7 @@ def test_impulsedict(krusell_smith_dag): temp = ir_lin[['C', 'K']] | ir_lin[['r']] assert list(temp.impulse.keys()) == ['C', 'K', 'r'] - # Normalize and scalar multiplication + # SS and scalar multiplication dC1 = 100 * ir_lin['C'] / ss['C'] - dC2 = 100 * ir_lin[['C']].normalize() + dC2 = 100 * ir_lin[['C']] / ss assert np.allclose(dC1, dC2['C']) - - # Levels and deviations - assert np.linalg.norm(ir_nonlin.deviations()['C'] - ir_lin['C']) < 1E-4 - assert np.linalg.norm(ir_nonlin['C'] - ir_lin.levels()['C']) < 1E-4 From c1e6f3d65734724eff611c8742a0b0126c8b4db1 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 15:14:52 -0500 Subject: [PATCH 151/288] Nonlinear impulse responses default to deviations. --- sequence_jacobian/blocks/combined_block.py | 4 ++-- sequence_jacobian/blocks/het_block.py | 6 +++--- sequence_jacobian/blocks/simple_block.py | 2 +- sequence_jacobian/nonlinear.py | 4 ++-- sequence_jacobian/primitives.py | 2 +- tests/base/test_simple_block.py | 9 +++++---- tests/base/test_transitional_dynamics.py | 10 +++++----- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py index 9658c47..3835b1f 100644 --- a/sequence_jacobian/blocks/combined_block.py +++ b/sequence_jacobian/blocks/combined_block.py @@ -76,9 +76,9 @@ def impulse_nonlinear(self, ss, exogenous, **kwargs): input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update({k: v - ss[k] for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) - return ImpulseDict(irf_nonlin_partial_eq) + ss + return ImpulseDict(irf_nonlin_partial_eq) def impulse_linear(self, ss, exogenous, T=None, Js=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py index a4b0c55..9a2c89d 100644 --- a/sequence_jacobian/blocks/het_block.py +++ b/sequence_jacobian/blocks/het_block.py @@ -258,7 +258,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal Parameters ---------- - ss : dict + ss : SteadyStateDict all steady-state info, intended to be from .ss() exogenous : dict of {str : array(T, ...)} all time-varying inputs here (in deviations), with first dimension being time @@ -356,9 +356,9 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal # return either this, or also include distributional information if returnindividual: return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, - 'D': D_path}) + 'D': D_path}) - ss else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs}) + return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): # infer T from exogenous, check that all shocks have same length diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py index a0689fa..c323e2d 100644 --- a/sequence_jacobian/blocks/simple_block.py +++ b/sequence_jacobian/blocks/simple_block.py @@ -79,7 +79,7 @@ def impulse_nonlinear(self, ss, exogenous): if k not in input_args: input_args[k] = ignore(ss[k]) - return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) + return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) - ss def impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py index 2d9bc97..b45ef9e 100644 --- a/sequence_jacobian/nonlinear.py +++ b/sequence_jacobian/nonlinear.py @@ -45,7 +45,7 @@ def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=Fa break # initialize guess for unknowns to steady state length T - unknown_paths = {k: np.full(T, ss[k]) for k in unknowns} + unknown_paths = {k: np.zeros(T) for k in unknowns} Uvec = pack_vectors(unknown_paths, unknowns, T) # obtain Jacobian of targets wrt to unknowns @@ -105,7 +105,7 @@ def td_map(block_list, ss, exogenous, unknowns=None, sort=None, # if any input to the block has changed, run the block if not block.inputs.isdisjoint(results): - results.update(block.impulse_nonlinear(ss, exogenous={k: results[k] - ss[k] for k in block.inputs if k in results}, + results.update(block.impulse_nonlinear(ss, exogenous={k: results[k] for k in block.inputs if k in results}, **{k: v for k, v in hetoptions.items() if k in misc.input_kwarg_list(block.impulse_nonlinear)})) diff --git a/sequence_jacobian/primitives.py b/sequence_jacobian/primitives.py index 6097345..15a2def 100644 --- a/sequence_jacobian/primitives.py +++ b/sequence_jacobian/primitives.py @@ -107,7 +107,7 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] irf_nonlin_gen_eq = td_solve(blocks, ss, - exogenous={k: ss[k] + v for k, v in exogenous.items()}, + exogenous={k: v for k, v in exogenous.items()}, unknowns=unknowns, targets=targets, Js=Js, **kwargs) return ImpulseDict(irf_nonlin_gen_eq) diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 781ecd4..6610dba 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -1,4 +1,5 @@ """Test SimpleBlock functionality""" +import copy import numpy as np import pytest @@ -40,9 +41,9 @@ def test_block_consistency(block, ss): ss_results = block.ss(**ss) # now if we put in constant inputs, td should give us the same! - td_results = block.td(ss, **{k: np.zeros(20) for k in ss.keys()}) + td_results = block.td(ss_results, **{k: np.zeros(20) for k in ss.keys()}) for k, v in td_results.impulse.items(): - assert np.all(v == ss_results[k]) + assert np.all(v == 0) # now get the Jacobian J = block.jac(ss, shock_list=block.input_list) @@ -53,8 +54,8 @@ def test_block_consistency(block, ss): h = 1E-5 all_shocks = {i: np.random.rand(10) for i in block.input_list} - td_up = block.td(ss, **{i: h*shock for i, shock in all_shocks.items()}) - td_dn = block.td(ss, **{i: -h*shock for i, shock in all_shocks.items()}) + td_up = block.td(ss_results, **{i: h*shock for i, shock in all_shocks.items()}) + td_dn = block.td(ss_results, **{i: -h*shock for i, shock in all_shocks.items()}) linear_impulses = {o: (td_up.impulse[o] - td_dn.impulse[o])/(2*h) for o in td_up.impulse} linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up.impulse} diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index a34fc2f..613453f 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -22,8 +22,8 @@ def test_rbc_td(rbc_dag): td_nonlin = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 0]}, unknowns=unknowns, targets=targets) td_nonlin_news = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 1]}, unknowns=unknowns, targets=targets) - dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) - dC_nonlin_news = 100 * (td_nonlin_news['C'] / ss['C'] - 1) + dC_nonlin = 100 * td_nonlin['C'] / ss['C'] + dC_nonlin_news = 100 * td_nonlin_news['C'] / ss['C'] assert np.linalg.norm(dC[:, 0] - dC_nonlin, np.inf) < 3e-2 assert np.linalg.norm(dC[:, 1] - dC_nonlin_news, np.inf) < 7e-2 @@ -40,7 +40,7 @@ def test_ks_td(krusell_smith_dag): td_nonlin = ks_model.solve_impulse_nonlinear(ss, {"Z": dZ}, unknowns=unknowns, targets=targets, monotonic=True) - dr_nonlin = 10000 * (td_nonlin['r'] - ss['r']) + dr_nonlin = 10000 * td_nonlin['r'] dr_lin = 10000 * G['r']['Z'] @ dZ assert np.linalg.norm(dr_nonlin - dr_lin, np.inf) < tol @@ -59,7 +59,7 @@ def test_hank_td(one_asset_hank_dag): td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, Js={'household': J_ha}) - dC_nonlin = 100 * (td_nonlin['C'] / ss['C'] - 1) + dC_nonlin = 100 * td_nonlin['C'] / ss['C'] dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] assert np.linalg.norm(dC_nonlin - dC_lin, np.inf) < 3e-3 @@ -79,7 +79,7 @@ def test_two_asset_td(two_asset_hank_dag): td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, Js={'household': J_ha}) - dY_nonlin = 100 * (td_nonlin['Y'] - 1) + dY_nonlin = 100 * td_nonlin['Y'] dY_lin = 100 * G['Y']['rstar'] @ drstar assert np.linalg.norm(dY_nonlin - dY_lin, np.inf) < tol From 4ae80936c9cff43894caf0beeda36e30094c1c89 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 15:22:20 -0500 Subject: [PATCH 152/288] Throw out old ImpulseDict methods. --- sequence_jacobian/blocks/support/impulse.py | 20 -------------------- tests/base/test_public_classes.py | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py index 907819e..0d29362 100644 --- a/sequence_jacobian/blocks/support/impulse.py +++ b/sequence_jacobian/blocks/support/impulse.py @@ -67,23 +67,3 @@ def __truediv__(self, other): # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero if isinstance(other, SteadyStateDict): return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) - - # def normalize(self, x=None): - # if x is None: - # # default: normalize by steady state if not zero - # impulse = {k: v/self.ss[k] if not np.isclose(self.ss[k], 0) else v for k, v in self.impulse.items()} - # else: - # # normalize by steady state of x - # if x not in self.ss.keys(): - # raise ValueError(f'Cannot normalize with {x}: steady state is unknown.') - # elif np.isclose(self.ss[x], 0): - # raise ValueError(f'Cannot normalize with {x}: steady state is zero.') - # else: - # impulse = {k: v / self.ss[x] for k, v in self.impulse.items()} - # return type(self)(impulse, self.ss) - # - # def levels(self): - # return type(self)({k: v + self.ss[k] for k, v in self.impulse.items()}, self.ss) - # - # def deviations(self): - # return type(self)({k: v - self.ss[k] for k, v in self.impulse.items()}, self.ss) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 9af4a81..57a86e8 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -56,7 +56,7 @@ def test_impulsedict(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 200 - # Linearized impulse responses as deviations, nonlinear as levels + # Linearized impulse responses as deviations ir_lin = ks_model.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) # Get method From 2502f7eb84c828fedc41e4664eddadd6237b917f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 16:25:49 -0500 Subject: [PATCH 153/288] Merged manually with Michael's `src` restructuring from today. --- requirements.txt | 3 - sequence_jacobian/__init__.py | 31 - sequence_jacobian/blocks/__init__.py | 1 - .../blocks/auxiliary_blocks/__init__.py | 1 - .../auxiliary_blocks/jacobiandict_block.py | 26 - sequence_jacobian/blocks/combined_block.py | 131 --- sequence_jacobian/blocks/het_block.py | 922 ------------------ sequence_jacobian/blocks/simple_block.py | 170 ---- sequence_jacobian/blocks/solved_block.py | 126 --- sequence_jacobian/blocks/support/__init__.py | 2 - sequence_jacobian/blocks/support/impulse.py | 69 -- .../blocks/support/simple_displacement.py | 702 ------------- sequence_jacobian/devtools/__init__.py | 3 - sequence_jacobian/devtools/analysis.py | 242 ----- sequence_jacobian/devtools/debug.py | 106 -- sequence_jacobian/devtools/deprecate.py | 22 - sequence_jacobian/devtools/upgrade.py | 39 - sequence_jacobian/estimation.py | 86 -- sequence_jacobian/jacobian/__init__.py | 1 - sequence_jacobian/jacobian/classes.py | 471 --------- sequence_jacobian/jacobian/drivers.py | 261 ----- sequence_jacobian/jacobian/support.py | 100 -- sequence_jacobian/models/__init__.py | 1 - sequence_jacobian/models/hank.py | 223 ----- sequence_jacobian/models/krusell_smith.py | 125 --- sequence_jacobian/models/rbc.py | 95 -- sequence_jacobian/models/two_asset.py | 423 -------- sequence_jacobian/nonlinear.py | 112 --- sequence_jacobian/primitives.py | 137 --- sequence_jacobian/steady_state/__init__.py | 1 - sequence_jacobian/steady_state/classes.py | 66 -- sequence_jacobian/steady_state/drivers.py | 306 ------ sequence_jacobian/steady_state/support.py | 253 ----- sequence_jacobian/utilities/__init__.py | 3 - sequence_jacobian/utilities/differentiate.py | 56 -- sequence_jacobian/utilities/discretize.py | 139 --- sequence_jacobian/utilities/forward_step.py | 175 ---- sequence_jacobian/utilities/graph.py | 281 ------ sequence_jacobian/utilities/interpolate.py | 185 ---- sequence_jacobian/utilities/misc.py | 175 ---- .../utilities/optimized_routines.py | 45 - sequence_jacobian/utilities/solvers.py | 148 --- sequence_jacobian/visualization/__init__.py | 1 - sequence_jacobian/visualization/draw_dag.py | 161 --- setup.py | 6 +- 45 files changed, 4 insertions(+), 6628 deletions(-) delete mode 100644 sequence_jacobian/__init__.py delete mode 100644 sequence_jacobian/blocks/__init__.py delete mode 100644 sequence_jacobian/blocks/auxiliary_blocks/__init__.py delete mode 100644 sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py delete mode 100644 sequence_jacobian/blocks/combined_block.py delete mode 100644 sequence_jacobian/blocks/het_block.py delete mode 100644 sequence_jacobian/blocks/simple_block.py delete mode 100644 sequence_jacobian/blocks/solved_block.py delete mode 100644 sequence_jacobian/blocks/support/__init__.py delete mode 100644 sequence_jacobian/blocks/support/impulse.py delete mode 100644 sequence_jacobian/blocks/support/simple_displacement.py delete mode 100644 sequence_jacobian/devtools/__init__.py delete mode 100644 sequence_jacobian/devtools/analysis.py delete mode 100644 sequence_jacobian/devtools/debug.py delete mode 100644 sequence_jacobian/devtools/deprecate.py delete mode 100644 sequence_jacobian/devtools/upgrade.py delete mode 100644 sequence_jacobian/estimation.py delete mode 100644 sequence_jacobian/jacobian/__init__.py delete mode 100644 sequence_jacobian/jacobian/classes.py delete mode 100644 sequence_jacobian/jacobian/drivers.py delete mode 100644 sequence_jacobian/jacobian/support.py delete mode 100644 sequence_jacobian/models/__init__.py delete mode 100644 sequence_jacobian/models/hank.py delete mode 100644 sequence_jacobian/models/krusell_smith.py delete mode 100644 sequence_jacobian/models/rbc.py delete mode 100644 sequence_jacobian/models/two_asset.py delete mode 100644 sequence_jacobian/nonlinear.py delete mode 100644 sequence_jacobian/primitives.py delete mode 100644 sequence_jacobian/steady_state/__init__.py delete mode 100644 sequence_jacobian/steady_state/classes.py delete mode 100644 sequence_jacobian/steady_state/drivers.py delete mode 100644 sequence_jacobian/steady_state/support.py delete mode 100644 sequence_jacobian/utilities/__init__.py delete mode 100644 sequence_jacobian/utilities/differentiate.py delete mode 100644 sequence_jacobian/utilities/discretize.py delete mode 100644 sequence_jacobian/utilities/forward_step.py delete mode 100644 sequence_jacobian/utilities/graph.py delete mode 100644 sequence_jacobian/utilities/interpolate.py delete mode 100644 sequence_jacobian/utilities/misc.py delete mode 100644 sequence_jacobian/utilities/optimized_routines.py delete mode 100644 sequence_jacobian/utilities/solvers.py delete mode 100644 sequence_jacobian/visualization/__init__.py delete mode 100644 sequence_jacobian/visualization/draw_dag.py diff --git a/requirements.txt b/requirements.txt index 3c3e53f..1f49b43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,3 @@ numpy>=1.18 scipy>=1.5 numba>=0.50 xarray>=0.17 - -pytest>=5.4 -setuptools>=49.2 \ No newline at end of file diff --git a/sequence_jacobian/__init__.py b/sequence_jacobian/__init__.py deleted file mode 100644 index 2f7abb6..0000000 --- a/sequence_jacobian/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Public-facing objects.""" - -from . import estimation, jacobian, nonlinear, utilities, devtools - -from .models import rbc, krusell_smith, hank, two_asset - -from .blocks.simple_block import simple -from .blocks.het_block import het, hetoutput -from .blocks.solved_block import solved -from .blocks.combined_block import combine, create_model -from .blocks.support.simple_displacement import apply_function - -from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved - -from .steady_state.drivers import steady_state -from .jacobian.drivers import get_G, get_H_U, get_impulse -from .nonlinear import td_solve - -# Useful utilities for setting up HetBlocks -from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen -from .utilities.interpolate import interpolate_y -from .utilities.optimized_routines import setmin - -# Ensure warning uniformity across package -import warnings - -# Force warnings.warn() to omit the source code line in the message -formatwarning_orig = warnings.formatwarning -warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ - formatwarning_orig(message, category, filename, lineno, line='') - diff --git a/sequence_jacobian/blocks/__init__.py b/sequence_jacobian/blocks/__init__.py deleted file mode 100644 index b33d734..0000000 --- a/sequence_jacobian/blocks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Block-construction tools""" \ No newline at end of file diff --git a/sequence_jacobian/blocks/auxiliary_blocks/__init__.py b/sequence_jacobian/blocks/auxiliary_blocks/__init__.py deleted file mode 100644 index d4a7666..0000000 --- a/sequence_jacobian/blocks/auxiliary_blocks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Auxiliary Block types for building a coherent backend for Block handling""" diff --git a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py deleted file mode 100644 index 227779d..0000000 --- a/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ /dev/null @@ -1,26 +0,0 @@ -"""A simple wrapper for JacobianDicts to be embedded in DAGs""" - -from numbers import Real -from typing import Dict, Union, List - -from ...primitives import Block, Array -from ...jacobian.classes import JacobianDict - - -class JacobianDictBlock(JacobianDict, Block): - """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" - def __init__(self, nesteddict, outputs=None, inputs=None, name=None): - super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) - - def __repr__(self): - return f"" - - def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: - return self.jacobian(list(exogenous.keys())).apply(exogenous) - - def jacobian(self, exogenous: List[str] = None, **kwargs) -> JacobianDict: - if exogenous is None: - return JacobianDict(self.nesteddict) - else: - return self[:, exogenous] diff --git a/sequence_jacobian/blocks/combined_block.py b/sequence_jacobian/blocks/combined_block.py deleted file mode 100644 index 3835b1f..0000000 --- a/sequence_jacobian/blocks/combined_block.py +++ /dev/null @@ -1,131 +0,0 @@ -"""CombinedBlock class and the combine function to generate it""" - -from copy import deepcopy - -from .support.impulse import ImpulseDict -from ..primitives import Block -from .. import utilities as utils -from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock -from ..steady_state.drivers import eval_block_ss -from ..steady_state.support import provide_solver_default -from ..jacobian.classes import JacobianDict - - -def combine(blocks, name="", model_alias=False): - return CombinedBlock(blocks, name=name, model_alias=model_alias) - - -# Useful functional alias -def create_model(blocks, **kwargs): - return combine(blocks, model_alias=True, **kwargs) - - -class CombinedBlock(Block): - """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides - a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, - and calculates Jacobians along the DAG""" - # To users: Do *not* manually change the attributes via assignment. Instantiating a - # CombinedBlock has some automated features that are inferred from initial instantiation but not from - # re-assignment of attributes post-instantiation. - def __init__(self, blocks, name="", model_alias=False): - - self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] - self._sorted_indices = utils.graph.block_sort(blocks) - self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) - self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] - - if not name: - self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" - else: - self.name = name - - # Find all outputs (including those used as intermediary inputs) - self.outputs = set().union(*[block.outputs for block in self.blocks]) - - # Find all inputs that are *not* intermediary outputs - all_inputs = set().union(*[block.inputs for block in self.blocks]) - self.inputs = all_inputs.difference(self.outputs) - - # If the create_model() is used instead of combine(), we will have __repr__ show this object as a 'Model' - self._model_alias = model_alias - - def __repr__(self): - if self._model_alias: - return f"" - else: - return f"" - - def steady_state(self, calibration, helper_blocks=None, **kwargs): - """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" - if helper_blocks is None: - helper_blocks = [] - - topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) - blocks_all = self.blocks + helper_blocks - - ss_partial_eq = deepcopy(calibration) - for i in topsorted: - ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) - return ss_partial_eq - - def impulse_nonlinear(self, ss, exogenous, **kwargs): - """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from - a steady state, `ss`""" - irf_nonlin_partial_eq = deepcopy(exogenous) - for block in self.blocks: - input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} - - if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) - - return ImpulseDict(irf_nonlin_partial_eq) - - def impulse_linear(self, ss, exogenous, T=None, Js=None): - """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from - a steady_state, `ss`""" - irf_lin_partial_eq = deepcopy(exogenous) - for block in self.blocks: - input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} - - if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T, Js=Js)}) - - return ImpulseDict(irf_lin_partial_eq) - - def jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): - """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at - a steady state, `ss`""" - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None: - outputs = self.outputs - kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "Js": Js} - - for i, block in enumerate(self.blocks): - curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() - - # If we want specific list of outputs, restrict curlyJ to that before continuing - curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] - if i == 0: - J_partial_eq = curlyJ.compose(JacobianDict.identity(exogenous)) - else: - J_partial_eq.update(curlyJ.compose(J_partial_eq)) - - return J_partial_eq - - def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, - sort_blocks=False, **kwargs): - """Evaluate a general equilibrium steady state of the CombinedBlock given a `calibration` - and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and - the target conditions that must hold in general equilibrium""" - if solver is None: - solver = provide_solver_default(unknowns) - if helper_blocks and sort_blocks is False: - sort_blocks = True - - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, - helper_blocks=helper_blocks, sort_blocks=sort_blocks, **kwargs) - - -# Useful type aliases -Model = CombinedBlock diff --git a/sequence_jacobian/blocks/het_block.py b/sequence_jacobian/blocks/het_block.py deleted file mode 100644 index 9a2c89d..0000000 --- a/sequence_jacobian/blocks/het_block.py +++ /dev/null @@ -1,922 +0,0 @@ -import warnings -import copy -import numpy as np - -from .support.impulse import ImpulseDict -from ..primitives import Block -from .. import utilities as utils -from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict -from ..devtools.deprecate import rename_output_list_to_outputs -from ..utilities.misc import verify_saved_jacobian - - -def het(exogenous, policy, backward, backward_init=None): - def decorator(back_step_fun): - return HetBlock(back_step_fun, exogenous, policy, backward, backward_init=backward_init) - return decorator - - -class HetBlock(Block): - """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. - - IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since - the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle - aggregation of non-aggregate outputs across the distribution and return aggregates as upper-case equivalents - of the `policy` and non-aggregate output variables specified in the backward step function. - """ - - def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None): - """Construct HetBlock from backward iteration function. - - Parameters - ---------- - back_step_fun : function - backward iteration function - exogenous : str - name of Markov transition matrix for exogenous variable - (now only single allowed for simplicity; use Kronecker product for more) - policy : str or sequence of str - names of policy variables of endogenous, continuous state variables - e.g. assets 'a', must be returned by function - backward : str or sequence of str - variables that together comprise the 'v' that we use for iterating backward - must appear both as outputs and as arguments - - It is assumed that every output of the function (except possibly backward), including policy, - will be on a grid of dimension 1 + len(policy), where the first dimension is the exogenous - variable and then the remaining dimensions are each of the continuous policy variables, in the - same order they are listed in 'policy'. - - The Markov transition matrix between the current and future period and backward iteration - variables should appear in the backward iteration function with '_p' subscripts ("prime") to - indicate that they come from the next period. - - Currently, we only support up to two policy variables. - """ - self.name = back_step_fun.__name__ - - # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. - # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) - self.back_step_fun = back_step_fun - - # self.back_step_outputs and self.back_step_inputs are all of the output and input arguments of - # self.back_step_fun, the variables used in the backward iteration, - # which generally include value and/or policy functions. - self.back_step_output_list = utils.misc.output_list(back_step_fun) - self.back_step_outputs = set(self.back_step_output_list) - self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) - - # See the docstring of HetBlock for details on the attributes directly below - self.exogenous = exogenous - self.policy, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (policy, backward)) - - # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" - # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun - # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to - # the primed key name, such that self.back_step_fun will properly call those variables. - # e.g. the key "Va" will become "Va_p", associated to the same value. - self.inputs_to_be_primed = {self.exogenous} | set(self.back_iter_vars) - - # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward - # iteration variables themselves. - self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) - - # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used - # in utils.graph.block_sort to topologically sort blocks along the DAG - # according to their aggregate outputs and inputs. - self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} - self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} - self.inputs.remove(exogenous + '_p') - self.inputs.add(exogenous) - - # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. - # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. - self.hetinput = None - self.hetinput_inputs = set() - self.hetinput_outputs = set() - self.hetinput_outputs_order = tuple() - - # start without a hetoutput - self.hetoutput = None - self.hetoutput_inputs = set() - self.hetoutput_outputs = set() - self.hetoutput_outputs_order = tuple() - - # The set of variables that will be wrapped in a separate namespace for this HetBlock - # as opposed to being available at the top level - self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} - - if len(self.policy) > 2: - raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") - - # Checking that the various inputs/outputs attributes are correctly set - if self.exogenous + '_p' not in self.back_step_inputs: - raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") - - for pol in self.policy: - if pol not in self.back_step_outputs: - raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") - if pol[0].isupper(): - raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") - - for back in self.back_iter_vars: - if back + '_p' not in self.back_step_inputs: - raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") - - if back not in self.back_step_outputs: - raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") - - for out in self.non_back_iter_outputs: - if out[0].isupper(): - raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") - - # Add the backward iteration initializer function (the initial guesses for self.back_iter_vars) - if backward_init is None: - # TODO: Think about implementing some "automated way" of providing - # an initial guess for the backward iteration. - self.backward_init = backward_init - else: - self.backward_init = backward_init - - # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. - - def __repr__(self): - """Nice string representation of HetBlock for printing to console""" - if self.hetinput is not None: - if self.hetoutput is not None: - return f"" - else: - return f"" - else: - return f"" - - '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - - ss : do backward and forward iteration until convergence to get complete steady state - - td : do backward and forward iteration up to T to compute dynamics given some shocks - - jac : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - - ajac : compute asymptotic columns of jacobians output by jac, also using fake news algorithm - - - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput - - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after - each backward iteration step in td - ''' - - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, **kwargs) - - def jac(self, ss, shock_list=None, T=None, **kwargs): - if shock_list is None: - shock_list = list(self.inputs) - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, shock_list, T, **kwargs) - - def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): - """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. - - Parameters - ---------- - backward_tol : [optional] float - in backward iteration, max abs diff between policy in consecutive steps needed for convergence - backward_maxit : [optional] int - maximum number of backward iterations, if 'backward_tol' not reached by then, raise error - forward_tol : [optional] float - in forward iteration, max abs diff between dist in consecutive steps needed for convergence - forward_maxit : [optional] int - maximum number of forward iterations, if 'forward_tol' not reached by then, raise error - - kwargs : dict - The following inputs are required as keyword arguments, which show up in 'kwargs': - - The exogenous Markov matrix, e.g. Pi=... if self.exogenous=='Pi' - - A seed for each backward variable, e.g. Va=... and Vb=... if self.back_iter_vars==('Va','Vb') - - A grid for each policy variable, e.g. a_grid=... and b_grid=... if self.policy==('a','b') - - All other inputs to the backward iteration function self.back_step_fun, except _p added to - for self.exogenous and self.back_iter_vars, for which the method uses steady-state values. - If there is a self.hetinput, then we need the inputs to that, not to self.back_step_fun. - - Other inputs in 'kwargs' are optional: - - A seed for the distribution: D=... - - If no seed for the distribution is provided, a seed for the invariant distribution - of the Markov process, e.g. Pi_seed=... if self.exogenous=='Pi' - - Returns - ---------- - ss : dict, contains - - ss inputs of self.back_step_fun and (if present) self.hetinput - - ss outputs of self.back_step_fun - - ss distribution 'D' - - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars - """ - - ss = copy.deepcopy(calibration) - - # extract information from calibration - Pi = calibration[self.exogenous] - grid = {k: calibration[k+'_grid'] for k in self.policy} - D_seed = calibration.get('D', None) - pi_seed = calibration.get(self.exogenous + '_seed', None) - - # run backward iteration - sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) - ss.update(sspol) - - # run forward iteration - D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) - ss.update({"D": D}) - - # aggregate all outputs other than backward variables on grid, capitalize - aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} - ss.update(aggregates) - - if hetoutput and self.hetoutput is not None: - hetoutputs = self.hetoutput.evaluate(ss) - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") - else: - hetoutputs = {} - aggregate_hetoutputs = {} - ss.update({**hetoutputs, **aggregate_hetoutputs}) - - return SteadyStateDict(ss, internal=self) - - def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): - """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, - assuming that we start and end in steady state ss, and that all inputs not specified in - kwargs are constant at their ss values. Analog to SimpleBlock.td. - - CANNOT provide time-varying paths of grid or Markov transition matrix for now. - - Parameters - ---------- - ss : SteadyStateDict - all steady-state info, intended to be from .ss() - exogenous : dict of {str : array(T, ...)} - all time-varying inputs here (in deviations), with first dimension being time - this must have same length T for all entries (all outputs will be calculated up to T) - monotonic : [optional] bool - flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us - to use faster interpolation routines, otherwise use slower robust to nonmonotonicity - returnindividual : [optional] bool - return distribution and full outputs on grid - grid_paths: [optional] dict of {str: array(T, Number of grid points)} - time-varying grids for policies - - Returns - ---------- - td : dict - if returnindividual = False, time paths for aggregates (uppercase) for all outputs - of self.back_step_fun except self.back_iter_vars - if returnindividual = True, additionally time paths for distribution and for all outputs - of self.back_Step_fun on the full grid - """ - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - # copy from ss info - Pi_T = ss[self.exogenous].T.copy() - D = ss.internal[self.name]['D'] - - # construct grids for policy variables either from the steady state grid if the grid is meant to be - # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. - grid = {} - use_ss_grid = {} - for k in self.policy: - if grid_paths is not None and k in grid_paths: - grid[k] = grid_paths[k] - use_ss_grid[k] = False - else: - grid[k] = ss[k+"_grid"] - use_ss_grid[k] = True - - # allocate empty arrays to store result, assume all like D - individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} - hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} - - # backward iteration - backdict = dict(ss.items()) - backdict.update(copy.deepcopy(ss.internal[self.name])) - for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) - individual = {k: v for k, v in zip(self.back_step_output_list, - self.back_step_fun(**self.make_inputs(backdict)))} - backdict.update({k: individual[k] for k in self.back_iter_vars}) - - if self.hetoutput is not None: - hetoutput = self.hetoutput.evaluate(backdict) - for k in self.hetoutput_outputs: - hetoutput_paths[k][t, ...] = hetoutput[k] - - for k in self.non_back_iter_outputs: - individual_paths[k][t, ...] = individual[k] - - D_path = np.empty((T,) + D.shape) - D_path[0, ...] = D - for t in range(T-1): - # have to interpolate policy separately for each t to get sparse transition matrices - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - if use_ss_grid[pol]: - grid_var = grid[pol] - else: - grid_var = grid[pol][t, ...] - if monotonic: - # TODO: change for two-asset case so assumption is monotonicity in own asset, not anything else - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord(grid_var, - individual_paths[pol][t, ...]) - else: - sspol_i[pol], sspol_pi[pol] =\ - utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[pol][t, ...]) - - # step forward - D_path[t+1, ...] = self.forward_step(D_path[t, ...], Pi_T, sspol_i, sspol_pi) - - # obtain aggregates of all outputs, made uppercase - aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) - for o in self.non_back_iter_outputs} - if self.hetoutput: - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutput_paths, D_path, backdict, mode="td") - else: - aggregate_hetoutputs = {} - - # return either this, or also include distributional information - if returnindividual: - return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, - 'D': D_path}) - ss - else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - - def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - - def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): - """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. - - Parameters - ---------- - ss : dict, - all steady-state info, intended to be from .ss() - T : [optional] int - number of time periods for T*T Jacobian - exogenous : list of str - names of input variables to differentiate wrt (main cost scales with # of inputs) - outputs : list of str - names of output variables to get derivatives of, if not provided assume all outputs of - self.back_step_fun except self.back_iter_vars - h : [optional] float - h for numerical differentiation of backward iteration - Js : [optional] dict of {str: JacobianDict}} - supply saved Jacobians - - Returns - ------- - J : dict of {str: dict of {str: array(T,T)}} - J[o][i] for output o and input i gives T*T Jacobian of o with respect to i - """ - # The default set of outputs are all outputs of the backward iteration function - # except for the backward iteration variables themselves - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None or output_list is None: - outputs = self.non_back_iter_outputs - else: - outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) - - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] - - # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js is not None: - outputs_cap = [o.capitalize() for o in outputs] - if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): - return Js[self.name] - - # step 0: preliminary processing of steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) - - # step 1 of fake news algorithm - # compute curlyY and curlyD (backward iteration) for each input i - curlyYs, curlyDs = {}, {} - for i in relevant_shocks: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, - ss.internal[self.name]['D'], Pi.T.copy(), - sspol_i, sspol_pi, sspol_space, T, h, - ss_for_hetinput) - - # step 2 of fake news algorithm - # compute prediction vectors curlyP (forward iteration) for each outcome o - curlyPs = {} - for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) - - # steps 3-4 of fake news algorithm - # make fake news matrix and Jacobian for each outcome-input pair - F, J = {}, {} - for o in outputs: - for i in relevant_shocks: - if o.capitalize() not in F: - F[o.capitalize()] = {} - if o.capitalize() not in J: - J[o.capitalize()] = {} - F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) - J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) - - return JacobianDict(J, name=self.name) - - def add_hetinput(self, hetinput, overwrite=False, verbose=True): - """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetinput function. - - A `hetinput` is any non-scalar-valued input argument provided to the HetBlock's backward iteration function, - self.back_step_fun, which is of the same dimensions as the distribution of agents in the HetBlock over - the relevant idiosyncratic state variables, generally referred to as `D`. e.g. The one asset HANK model - example provided in the models directory of sequence_jacobian has a hetinput `T`, which is skill-specific - transfers. - """ - if self.hetinput is not None and overwrite is False: - raise ValueError('Trying to attach hetinput when one already exists!') - else: - if verbose: - if self.hetinput is not None and overwrite is True: - print(f"Overwriting current hetinput, {self.hetinput.__name__} with new hetinput," - f" {hetinput.__name__}!") - else: - print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") - - self.hetinput = hetinput - self.hetinput_inputs = set(utils.misc.input_list(hetinput)) - self.hetinput_outputs = set(utils.misc.output_list(hetinput)) - self.hetinput_outputs_order = utils.misc.output_list(hetinput) - - # modify inputs to include hetinput's additional inputs, remove outputs - self.inputs |= self.hetinput_inputs - self.inputs -= self.hetinput_outputs - - self.internal |= self.hetinput_outputs - - def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): - """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetoutput function. - - A `hetoutput` is any *non-scalar-value* output that the user might desire to be calculated from - the output arguments of the HetBlock's backward iteration function. Importantly, as of now the `hetoutput` - cannot be a function of time displaced values of the HetBlock's outputs but rather must be able to - be calculated from the outputs statically. e.g. The two asset HANK model example provided in the models - directory of sequence_jacobian has a hetoutput, `chi`, the adjustment costs for any initial level of assets - `a`, to any new level of assets `a'`. - """ - if self.hetoutput is not None and overwrite is False: - raise ValueError('Trying to attach hetoutput when one already exists!') - else: - if verbose: - if self.hetoutput is not None and overwrite is True: - print(f"Overwriting current hetoutput, {self.hetoutput.name} with new hetoutput," - f" {hetoutput.name}!") - else: - print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") - - self.hetoutput = hetoutput - self.hetoutput_inputs = set(hetoutput.input_list) - self.hetoutput_outputs = set(hetoutput.output_list) - self.hetoutput_outputs_order = hetoutput.output_list - - # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput - # and aggregating the hetoutput, but do not include: - # 1) objects computed within the HetBlock's backward iteration that enter into the hetoutput computation - # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) - # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation - # but is computed after the backward iteration - self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) - # Modify the HetBlock's outputs to include the aggregated hetoutputs - self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) - - self.internal |= self.hetoutput_outputs - - '''Part 3: components of ss(): - - policy_ss : backward iteration to get steady-state policies and other outcomes - - dist_ss : forward iteration to get steady-state distribution and compute aggregates - ''' - - def policy_ss(self, ssin, tol=1E-8, maxit=5000): - """Find steady-state policies and backward variables through backward iteration until convergence. - - Parameters - ---------- - ssin : dict - all steady-state inputs to back_step_fun, including seed values for backward variables - tol : [optional] float - max diff between consecutive iterations of policy variables needed for convergence - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - - Returns - ---------- - sspol : dict - all steady-state outputs of backward iteration, combined with inputs to backward iteration - """ - - # find initial values for backward iteration and account for hetinputs - original_ssin = ssin - ssin = self.make_inputs(ssin) - - old = {} - for it in range(maxit): - try: - # run and store results of backward iteration, which come as tuple, in dict - sspol = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**ssin))} - except KeyError as e: - print(f'Missing input {e} to {self.back_step_fun.__name__}!') - raise - - # only check convergence every 10 iterations for efficiency - if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) - for k in self.policy): - break - - # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration - old.update({k: sspol[k] for k in self.policy}) - ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) - else: - raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') - - # want to record inputs in ssin, but remove _p, add in hetinput inputs if there - for k in self.inputs_to_be_primed: - ssin[k] = ssin[k + '_p'] - del ssin[k + '_p'] - if self.hetinput is not None: - for k in self.hetinput_inputs: - if k in original_ssin: - ssin[k] = original_ssin[k] - return {**ssin, **sspol} - - def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): - """Find steady-state distribution through forward iteration until convergence. - - Parameters - ---------- - Pi : array - steady-state Markov matrix for exogenous variable - sspol : dict - steady-state policies on grid for all policy variables in self.policy - grid : dict - grids for all policy variables in self.policy - tol : [optional] float - absolute tolerance for max diff between consecutive iterations for distribution - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - D_seed : [optional] array - initial seed for overall distribution - pi_seed : [optional] array - initial seed for stationary dist of Pi, if no D_seed - - Returns - ---------- - D : array - steady-state distribution - """ - - # first obtain initial distribution D - if D_seed is None: - # compute stationary distribution for exogenous variable - pi = utils.discretize.stationary(Pi, pi_seed) - - # now initialize full distribution with this, assuming uniform distribution on endogenous vars - endogenous_dims = [grid[k].shape[0] for k in self.policy] - D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) - else: - D = D_seed - - # obtain interpolated policy rule for each dimension of endogenous policy - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - # use robust binary search-based method that only requires grids, not policies, to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], sspol[pol]) - - # iterate until convergence by tol, or maxit - Pi_T = Pi.T.copy() - for it in range(maxit): - Dnew = self.forward_step(D, Pi_T, sspol_i, sspol_pi) - - # only check convergence every 10 iterations for efficiency - if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): - break - D = Dnew - else: - raise ValueError(f'No convergence after {maxit} forward iterations!') - - return D - - '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper - - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs - - Step 2: forward_iteration_fakenews to get curlyPs - - Step 3: build_F to get fake news matrix from curlyYs, curlyDs, curlyPs - - Step 4: J_from_F to get Jacobian from fake news matrix - ''' - - def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): - # shock perturbs outputs - shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, - utils.differentiate.numerical_diff(self.back_step_fun, - ssin_dict, din_dict, h, - ssout_list))} - curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} - - # which affects the distribution tomorrow - pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} - - # Include an additional term to account for the effect of a deleveraging shock affecting the grid - if "delev_exante" in din_dict: - dx = np.zeros_like(sspol_pi["a"]) - dx[sspol_i["a"] == 0] = 1. - add_term = sspol_pi["a"] * dx / sspol_space["a"] - pol_pi_shock["a"] += add_term - - curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) - - # and the aggregate outcomes today - curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} - - return curlyV, curlyD, curlyY - - def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, - sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): - """Iterate policy steps backward T times for a single shock.""" - # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None - # since unless self.hetinput_inputs is exactly equal to input_shocked, calling - # self.hetinput() inside the symmetric differentiation function will throw an error. - # It's probably better/more informative to throw that error out here. - if self.hetinput is not None and input_shocked in self.hetinput_inputs: - # if input_shocked is an input to hetinput, take numerical diff to get response - din_dict = dict(zip(self.hetinput_outputs_order, - utils.differentiate.numerical_diff_symmetric(self.hetinput, - ss_for_hetinput, {input_shocked: 1}, h))) - else: - # otherwise, we just have that one shock - din_dict = {input_shocked: 1} - - # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) - - # infer dimensions from this and initialize empty arrays - curlyDs = np.empty((T,) + curlyD.shape) - curlyYs = {k: np.empty(T) for k in curlyY.keys()} - - # fill in current effect of shock - curlyDs[0, ...] = curlyD - for k in curlyY.keys(): - curlyYs[k][0] = curlyY[k] - - # fill in anticipation effects - for t in range(1, T): - curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) - for k in curlyY.keys(): - curlyYs[k][t] = curlyY[k] - - return curlyYs, curlyDs - - def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): - """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. - - Note we depart from definition in paper by applying the demeaning operator in addition to Lambda - at each step. This does not affect products with curlyD (which are the only way curlyPs enter - Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits - since curlyPs now go to zero for high t (used in paper in proof of Proposition 1). - """ - curlyPs = np.empty((T,) + o_ss.shape) - curlyPs[0, ...] = utils.misc.demean(o_ss) - for t in range(1, T): - curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], - Pi, pol_i_ss, pol_pi_ss)) - return curlyPs - - @staticmethod - def build_F(curlyYs, curlyDs, curlyPs): - T = curlyDs.shape[0] - Tpost = curlyPs.shape[0] - T + 2 - F = np.empty((Tpost + T - 1, T)) - F[0, :] = curlyYs - F[1:, :] = curlyPs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T - return F - - @staticmethod - def J_from_F(F): - J = F.copy() - for t in range(1, J.shape[1]): - J[1:, t] += J[:-1, t - 1] - return J - - '''Part 5: helpers for .jac and .ajac: preliminary processing''' - - def jac_prelim(self, ss): - """Helper that does preliminary processing of steady state for fake news algorithm. - - Parameters - ---------- - ss : dict, all steady-state info, intended to be from .ss() - - Returns - ---------- - ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step - Pi : array (S*S), Markov matrix for exogenous state - ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same - as steady-state numerically since SS convergence was to some tolerance threshold) - ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) - sspol_i : dict, indices on lower bracketing gridpoint for all in self.policy - sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy - sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy - """ - # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation - ssin_dict = self.make_inputs(ss) - Pi = ss[self.exogenous] - grid = {k: ss[k+'_grid'] for k in self.policy} - ssout_list = self.back_step_fun(**ssin_dict) - - ss_for_hetinput = None - if self.hetinput is not None: - ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} - - # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints - sspol_i = {} - sspol_pi = {} - sspol_space = {} - for pol in self.policy: - # use robust binary-search-based method that only requires grids to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) - sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - - toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) - - return toreturn - - '''Part 6: helper to extract inputs and potentially process them through hetinput''' - - def make_inputs(self, back_step_inputs_dict): - """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, - process stuff through self.hetinput first if it's there. - """ - input_dict = copy.deepcopy(back_step_inputs_dict) - - # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady - # state computation as well as for Jacobian/impulse evaluation for HetBlocks, - # where in the former the hetinputs and value function have yet to be computed, - # whereas in the latter they have already been computed - # and hence do not need to be recomputed. There may be room to clean this function up a bit. - if isinstance(back_step_inputs_dict, SteadyStateDict): - input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) - input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) - else: - # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include - # them as inputs for self.back_step_fun - if self.hetinput is not None: - outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] - for k in self.hetinput_inputs if k in input_dict})) - input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) - - # Check if there are entries in indict corresponding to self.inputs_to_be_primed. - # In particular, we are interested in knowing if an initial value - # for the backward iteration variable has been provided. - # If it has not been provided, then use self.backward_init to calculate the initial values. - if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): - initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] - input_dict.update(zip(utils.misc.output_list(self.backward_init), - utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) - - for i_p in self.inputs_to_be_primed: - input_dict[i_p + "_p"] = input_dict[i_p] - del input_dict[i_p] - - try: - return {k: input_dict[k] for k in self.back_step_inputs if k in input_dict} - except KeyError as e: - print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') - raise - - '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' - - def forward_step(self, D, Pi_T, pol_i, pol_pi): - """Update distribution, calling on 1d and 2d-specific compiled routines. - - Parameters - ---------- - D : array, beginning-of-period distribution - Pi_T : array, transpose Markov matrix - pol_i : dict, indices on lower bracketing gridpoint for all in self.policy - pol_pi : dict, weights on lower bracketing gridpoint for all in self.policy - - Returns - ---------- - Dnew : array, beginning-of-next-period distribution - """ - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_1d(D, Pi_T, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_2d(D, Pi_T, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - def forward_step_transpose(self, D, Pi, pol_i, pol_pi): - """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_transpose_1d(D, Pi, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_transpose_2d(D, Pi, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): - """Forward_step linearized with respect to pol_pi""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_shock_1d(Dss, Pi_T, pol_i_ss[p], pol_pi_shock[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_shock_2d(Dss, Pi_T, pol_i_ss[p1], pol_i_ss[p2], - pol_pi_ss[p1], pol_pi_ss[p2], - pol_pi_shock[p1], pol_pi_shock[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - -def hetoutput(custom_aggregation=None): - def decorator(f): - return HetOutput(f, custom_aggregation=custom_aggregation) - return decorator - - -class HetOutput: - def __init__(self, f, custom_aggregation=None): - self.name = f.__name__ - self.f = f - self.eval_input_list = utils.misc.input_list(f) - - self.custom_aggregation = custom_aggregation - self.agg_input_list = [] if custom_aggregation is None else utils.misc.input_list(custom_aggregation) - - # We are distinguishing between the eval_input_list and agg_input_list because custom aggregation may require - # certain arguments that are not required for simply evaluating the hetoutput - self.input_list = list(set(self.eval_input_list).union(set(self.agg_input_list))) - self.output_list = utils.misc.output_list(f) - - def evaluate(self, arg_dict): - hetoutputs = dict(zip(self.output_list, utils.misc.make_tuple(self.f(*[arg_dict[i] for i - in self.eval_input_list])))) - return hetoutputs - - def aggregate(self, hetoutputs, D, custom_aggregation_args, mode="ss"): - if self.custom_aggregation is not None: - hetoutputs_w_std_aggregation = list(set(self.output_list) - - set([utils.misc.uncapitalize(o) for o - in utils.misc.output_list(self.custom_aggregation)])) - hetoutputs_w_custom_aggregation = list(set(self.output_list) - set(hetoutputs_w_std_aggregation)) - else: - hetoutputs_w_std_aggregation = self.output_list - hetoutputs_w_custom_aggregation = [] - - # TODO: May need to check if this works properly for td - if self.custom_aggregation is not None: - hetoutputs_w_custom_aggregation_args = dict(zip(hetoutputs_w_custom_aggregation, - [hetoutputs[i] for i in hetoutputs_w_custom_aggregation])) - custom_agg_inputs = {"D": D, **hetoutputs_w_custom_aggregation_args, **custom_aggregation_args} - custom_aggregates = dict(zip([o.capitalize() for o in hetoutputs_w_custom_aggregation], - utils.misc.make_tuple(self.custom_aggregation(*[custom_agg_inputs[i] for i - in self.agg_input_list])))) - else: - custom_aggregates = {} - - if mode == "ss": - std_aggregates = {o.capitalize(): np.vdot(D, hetoutputs[o]) for o in hetoutputs_w_std_aggregation} - elif mode == "td": - std_aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D, hetoutputs[o]) - for o in hetoutputs_w_std_aggregation} - else: - raise RuntimeError(f"Mode {mode} is not supported in HetOutput aggregation. Choose either 'ss' or 'td'") - - return {**std_aggregates, **custom_aggregates} diff --git a/sequence_jacobian/blocks/simple_block.py b/sequence_jacobian/blocks/simple_block.py deleted file mode 100644 index c323e2d..0000000 --- a/sequence_jacobian/blocks/simple_block.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Class definition of a simple block""" - -import warnings -import numpy as np -from copy import deepcopy - -from .support.simple_displacement import ignore, Displace, AccumulatedDerivative -from .support.impulse import ImpulseDict -from ..primitives import Block -from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix -from ..utilities import misc - -'''Part 1: SimpleBlock class and @simple decorator to generate it''' - - -def simple(f): - return SimpleBlock(f) - - -class SimpleBlock(Block): - """Generated from simple block written in Dynare-ish style and decorated with @simple, e.g. - - @simple - def production(Z, K, L, alpha): - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return Y - - which is a SimpleBlock that takes in Z, K, L, and alpha, all of which can be either constants - or series, and implements a Cobb-Douglas production function, noting that for production today - we use the capital K(-1) determined yesterday. - - Key methods are .ss, .td, and .jac, like HetBlock. - """ - - def __init__(self, f): - self.f = f - self.name = f.__name__ - self.input_list = misc.input_list(f) - self.output_list = misc.output_list(f) - self.inputs = set(self.input_list) - self.outputs = set(self.output_list) - - def __repr__(self): - return f"" - - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, exogenous=kwargs) - - def jac(self, ss, T=None, shock_list=None): - if shock_list is None: - shock_list = [] - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, exogenous=shock_list, T=T) - - def steady_state(self, calibration): - input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} - output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ - misc.numeric_primitive(self.f(**input_args))] - return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) - - def impulse_nonlinear(self, ss, exogenous): - input_args = {} - for k, v in exogenous.items(): - if np.isscalar(v): - raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') - input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) - - for k in self.input_list: - if k not in input_args: - input_args[k] = ignore(ss[k]) - - return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) - ss - - def impulse_linear(self, ss, exogenous, T=None, Js=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) - - def jacobian(self, ss, exogenous=None, T=None, Js=None): - """Assemble nested dict of Jacobians - - Parameters - ---------- - ss : dict, - steady state values - exogenous : list of str, optional - names of input variables to differentiate wrt; if omitted, assume all inputs - T : int, optional - number of time periods for explicit T*T Jacobian - if omitted, more efficient SimpleSparse objects returned - Js : dict of {str: JacobianDict}, optional - pre-computed Jacobians - - Returns - ------- - J : dict of {str: dict of {str: array(T,T)}} - J[o][i] for output o and input i gives Jacobian of o with respect to i - This Jacobian is a SimpleSparse object or, if T specific, a T*T matrix, omitted by convention if zero - """ - - if exogenous is None: - exogenous = list(self.inputs) - - relevant_shocks = [i for i in self.inputs if i in exogenous] - - # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js is not None: - if misc.verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): - return Js[self.name] - - # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks - # are an input into the block), then return an empty dict - if not relevant_shocks: - return JacobianDict({}) - else: - invertedJ = {shock_name: {} for shock_name in relevant_shocks} - - # Loop over all inputs/shocks which we want to differentiate with respect to - for shock in relevant_shocks: - invertedJ[shock] = compute_single_shock_curlyJ(self.f, ss, shock) - - # Because we computed the Jacobian of all outputs with respect to each shock (invertedJ[i][o]), - # we need to loop back through to have J[o][i] to map for a given output `o`, shock `i`, - # the Jacobian curlyJ^{o,i}. - J = {o: {} for o in self.output_list} - for o in self.output_list: - for i in relevant_shocks: - # Keep zeros, so we can inspect supplied Jacobians for completeness - if not invertedJ[i][o] or invertedJ[i][o].iszero: - J[o][i] = ZeroMatrix() - else: - if T is not None: - J[o][i] = invertedJ[i][o].matrix(T) - else: - J[o][i] = invertedJ[i][o] - - return JacobianDict(J, name=self.name) - - -def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): - """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" - input_args = {i: ignore(steady_state_dict[i]) for i in misc.input_list(f)} - input_args[shock_name] = AccumulatedDerivative(f_value=steady_state_dict[shock_name]) - - J = {o: {} for o in misc.output_list(f)} - for o, o_name in zip(misc.make_tuple(f(**input_args)), misc.output_list(f)): - if isinstance(o, AccumulatedDerivative): - J[o_name] = SimpleSparse(o.elements) - - return J - - -def make_impulse_uniform_length(out, output_list): - # If the function has multiple outputs - if isinstance(out, tuple): - # Because we know at least one of the outputs in `out` must be of length T - T = np.max([np.size(o) for o in out]) - out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else - misc.numeric_primitive(o) for o in out] - return dict(zip(output_list, misc.make_tuple(out_unif_dim))) - else: - return dict(zip(output_list, misc.make_tuple(misc.numeric_primitive(out)))) diff --git a/sequence_jacobian/blocks/solved_block.py b/sequence_jacobian/blocks/solved_block.py deleted file mode 100644 index 3a2caa7..0000000 --- a/sequence_jacobian/blocks/solved_block.py +++ /dev/null @@ -1,126 +0,0 @@ -import warnings - -from ..primitives import Block -from ..blocks.simple_block import simple -from ..utilities import graph - - -def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): - """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: - - as @solved(unknowns=..., targets=...) decorator on a single SimpleBlock - - as function solved(blocklist=..., unknowns=..., targets=...) where blocklist - can be any list of blocks - """ - if block_list: - if not name: - name = f"{block_list[0].name}_to_{block_list[-1].name}_solved" - # ordinary call, not as decorator - return SolvedBlock(block_list, name, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) - else: - # call as decorator, return function of function - def singleton_solved_block(f): - return SolvedBlock([simple(f)], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) - return singleton_solved_block - - -class SolvedBlock(Block): - """SolvedBlocks are mini SHADE models embedded as blocks inside larger SHADE models. - - When creating them, we need to provide the basic ingredients of a SHADE model: the list of - blocks comprising the model, the list on unknowns, and the list of targets. - - When we use .jac to ask for the Jacobian of a SolvedBlock, we are really solving for the 'G' - matrices of the mini SHADE models, which then become the 'curlyJ' Jacobians of the block. - - Similarly, when we use .td to evaluate a SolvedBlock on a path, we are really solving for the - nonlinear transition path such that all internal targets of the mini SHADE model are zero. - """ - - def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): - # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. - self._blocks_unsorted = blocks - - # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks - # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort - # Hence, we will cache that info upon first invocation of the steady_state - self._sorted_indices_w_o_helpers = graph.block_sort(blocks) - self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated - self._required = graph.find_outputs_that_are_intermediate_inputs(blocks) - - # User-facing attributes for accessing blocks - # .blocks_w_helpers meant to only interface with steady_state functionality - # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) - self.blocks_w_helpers = None - self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] - - self.name = name - self.unknowns = unknowns - self.targets = targets - self.solver = solver - self.solver_kwargs = solver_kwargs - - # need to have inputs and outputs!!! - self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) - self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs - - def __repr__(self): - return f"" - - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, **kwargs) - - def jac(self, ss, T=None, shock_list=None, **kwargs): - if shock_list is None: - shock_list = [] - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, shock_list, T, **kwargs) - - def steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, - consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): - # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for HelperBlocks - if self._sorted_indices_w_helpers is None: - self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, helper_blocks=helper_blocks, - calibration=calibration) - self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] - - # Allow override of unknowns/solver, if one wants to evaluate the SolvedBlock at a particular set of - # unknown values akin to the steady_state method of Block - if unknowns is None: - unknowns = self.unknowns - if solver is None: - solver = self.solver - - return super().solve_steady_state(calibration, unknowns, self.targets, solver=solver, - consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) - - def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): - return super().solve_impulse_nonlinear(ss, exogenous=exogenous, - unknowns=list(self.unknowns.keys()), Js=Js, - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - - def impulse_linear(self, ss, exogenous, T=None, Js=None): - return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, Js=Js) - - def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None: - outputs = list(self.outputs) - relevant_shocks = [i for i in self.inputs if i in exogenous] - - return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, outputs=outputs, Js=Js) diff --git a/sequence_jacobian/blocks/support/__init__.py b/sequence_jacobian/blocks/support/__init__.py deleted file mode 100644 index 1e46635..0000000 --- a/sequence_jacobian/blocks/support/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Other classes and helpers to aid standard block functionality: .steady_state, .impulse_linear, .impulse_nonlinear, -.jacobian""" diff --git a/sequence_jacobian/blocks/support/impulse.py b/sequence_jacobian/blocks/support/impulse.py deleted file mode 100644 index 0d29362..0000000 --- a/sequence_jacobian/blocks/support/impulse.py +++ /dev/null @@ -1,69 +0,0 @@ -"""ImpulseDict class for manipulating impulse responses.""" - -import numpy as np - -from ...steady_state.classes import SteadyStateDict - - -class ImpulseDict: - def __init__(self, impulse): - if isinstance(impulse, ImpulseDict): - self.impulse = impulse.impulse - else: - if not isinstance(impulse, dict): - raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses.') - self.impulse = impulse - - def __repr__(self): - return f'' - - def __iter__(self): - return iter(self.impulse.items()) - - def __or__(self, other): - if not isinstance(other, ImpulseDict): - raise ValueError('Trying to merge an ImpulseDict with something else.') - # Union returns a new ImpulseDict - merged = type(self)(self.impulse) - merged.impulse.update(other.impulse) - return merged - - def __getitem__(self, item): - # Behavior similar to pandas - if isinstance(item, str): - # Case 1: ImpulseDict['C'] returns array - return self.impulse[item] - if isinstance(item, list): - # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts - return type(self)({k: self.impulse[k] for k in item}) - - def __add__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v + other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): - return type(self)({k: v + other[k] for k, v in self.impulse.items()}) - - def __sub__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v - other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): - return type(self)({k: v - other[k] for k, v in self.impulse.items()}) - - def __mul__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): - return type(self)({k: v * other[k] for k, v in self.impulse.items()}) - - def __rmul__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): - return type(self)({k: v * other[k] for k, v in self.impulse.items()}) - - def __truediv__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v / other for k, v in self.impulse.items()}) - # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero - if isinstance(other, SteadyStateDict): - return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) diff --git a/sequence_jacobian/blocks/support/simple_displacement.py b/sequence_jacobian/blocks/support/simple_displacement.py deleted file mode 100644 index 87ee155..0000000 --- a/sequence_jacobian/blocks/support/simple_displacement.py +++ /dev/null @@ -1,702 +0,0 @@ -"""Displacement handler classes used by SimpleBlock for .ss, .td, and .jac evaluation to have Dynare-like syntax""" - -import numpy as np -import numbers -from warnings import warn - -from ...utilities.misc import numeric_primitive - -def ignore(x): - if isinstance(x, int): - return IgnoreInt(x) - elif isinstance(x, numbers.Real) and not isinstance(x, int): - return IgnoreFloat(x) - elif isinstance(x, np.ndarray): - return IgnoreVector(x) - else: - raise TypeError(f"{type(x)} is not supported. Must provide either a float or an nd.array as an argument") - - -class IgnoreInt(int): - """This class ignores time displacements of a scalar. - Standard arithmetic operators including +, -, x, /, ** all overloaded to "promote" the result of - any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore - """ - - def __repr__(self): - return f'IgnoreInt({numeric_primitive(self)})' - - @property - def ss(self): - return self - - def __call__(self, index): - return self - - def apply(self, f, **kwargs): - return ignore(f(numeric_primitive(self), **kwargs)) - - def __pos__(self): - return self - - def __neg__(self): - return ignore(-numeric_primitive(self)) - - def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__radd__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) + other) - - def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__add__(numeric_primitive(self)) - else: - return ignore(other + numeric_primitive(self)) - - def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rsub__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) - other) - - def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__sub__(numeric_primitive(self)) - else: - return ignore(other - numeric_primitive(self)) - - def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rmul__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) * other) - - def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__mul__(numeric_primitive(self)) - else: - return ignore(other * numeric_primitive(self)) - - def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rtruediv__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) / other) - - def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__truediv__(numeric_primitive(self)) - else: - return ignore(other / numeric_primitive(self)) - - def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): - return power.__rpow__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) ** power) - - def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__pow__(numeric_primitive(self)) - else: - return ignore(other ** numeric_primitive(self)) - - -class IgnoreFloat(float): - """This class ignores time displacements of a scalar. - Standard arithmetic operators including +, -, x, /, ** all overloaded to "promote" the result of - any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore - """ - - def __repr__(self): - return f'IgnoreFloat({numeric_primitive(self)})' - - @property - def ss(self): - return self - - def __call__(self, index): - return self - - def apply(self, f, **kwargs): - return ignore(f(numeric_primitive(self), **kwargs)) - - def __pos__(self): - return self - - def __neg__(self): - return ignore(-numeric_primitive(self)) - - def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__radd__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) + other) - - def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__add__(numeric_primitive(self)) - else: - return ignore(other + numeric_primitive(self)) - - def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rsub__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) - other) - - def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__sub__(numeric_primitive(self)) - else: - return ignore(other - numeric_primitive(self)) - - def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rmul__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) * other) - - def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__mul__(numeric_primitive(self)) - else: - return ignore(other * numeric_primitive(self)) - - def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rtruediv__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) / other) - - def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__truediv__(numeric_primitive(self)) - else: - return ignore(other / numeric_primitive(self)) - - def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): - return power.__rpow__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) ** power) - - def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__pow__(numeric_primitive(self)) - else: - return ignore(other ** numeric_primitive(self)) - - -class IgnoreVector(np.ndarray): - """This class ignores time displacements of a np.ndarray. - See NumPy documentation on "Subclassing ndarray" for more details on the use of __new__ - for this implementation.""" - - def __new__(cls, x): - obj = np.asarray(x).view(cls) - return obj - - def __repr__(self): - return f'IgnoreVector({numeric_primitive(self)})' - - @property - def ss(self): - return self - - def __call__(self, index): - return self - - def apply(self, f, **kwargs): - return ignore(f(numeric_primitive(self), **kwargs)) - - def __add__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__radd__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) + other) - - def __radd__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__add__(numeric_primitive(self)) - else: - return ignore(other + numeric_primitive(self)) - - def __sub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rsub__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) - other) - - def __rsub__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__sub__(numeric_primitive(self)) - else: - return ignore(other - numeric_primitive(self)) - - def __mul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rmul__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) * other) - - def __rmul__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__mul__(numeric_primitive(self)) - else: - return ignore(other * numeric_primitive(self)) - - def __truediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__rtruediv__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) / other) - - def __rtruediv__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__truediv__(numeric_primitive(self)) - else: - return ignore(other / numeric_primitive(self)) - - def __pow__(self, power, modulo=None): - if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): - return power.__rpow__(numeric_primitive(self)) - else: - return ignore(numeric_primitive(self) ** power) - - def __rpow__(self, other): - if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): - return other.__pow__(numeric_primitive(self)) - else: - return ignore(other ** numeric_primitive(self)) - - -class Displace(np.ndarray): - """This class makes time displacements of a time path, given the steady-state value. - Needed for SimpleBlock.td()""" - - def __new__(cls, x, ss=None, name='UNKNOWN'): - obj = np.asarray(x).view(cls) - obj.ss = ss - obj.name = name - return obj - - def __repr__(self): - return f'Displace({numeric_primitive(self)})' - - # TODO: Implemented a very preliminary generalization of Displace to higher-dimensional (>1) ndarrays - # however the rigorous operator overloading/testing has not been checked for higher dimensions. - # Should also implement some checks for the dimension of .ss, to ensure that it's always N-1 - # where we also assume that the *last* dimension is the time dimension - def __call__(self, index): - if index != 0: - if self.ss is None: - raise KeyError(f'Trying to call {self.name}({index}), but steady-state {self.name} not given!') - newx = np.zeros(np.shape(self)) - if index > 0: - newx[..., :-index] = numeric_primitive(self)[..., index:] - newx[..., -index:] = self.ss - else: - newx[..., -index:] = numeric_primitive(self)[..., :index] - newx[..., :-index] = self.ss - return Displace(newx, ss=self.ss) - else: - return self - - def apply(self, f, **kwargs): - return Displace(f(numeric_primitive(self), **kwargs), ss=f(self.ss)) - - def __pos__(self): - return self - - def __neg__(self): - return Displace(-numeric_primitive(self), ss=-self.ss) - - def __add__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss + other.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss + numeric_primitive(other)) - else: - # TODO: See if there is a different, systematic way we want to handle this case. - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss) - - def __radd__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=other.ss + self.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=numeric_primitive(other) + self.ss) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=self.ss) - - def __sub__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss - other.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss - numeric_primitive(other)) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss) - - def __rsub__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=other.ss - self.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=numeric_primitive(other) - self.ss) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=self.ss) - - def __mul__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss * other.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss * numeric_primitive(other)) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss) - - def __rmul__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=other.ss * self.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=numeric_primitive(other) * self.ss) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=self.ss) - - def __truediv__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss / other.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss / numeric_primitive(other)) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss) - - def __rtruediv__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=other.ss / self.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=numeric_primitive(other) / self.ss) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=self.ss) - - def __pow__(self, power): - if isinstance(power, Displace): - return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss ** power.ss) - elif np.isscalar(power): - return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss ** numeric_primitive(power)) - else: - warn("\n" + f"Applying operation to {power}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss) - - def __rpow__(self, other): - if isinstance(other, Displace): - return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=other.ss ** self.ss) - elif np.isscalar(other): - return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=numeric_primitive(other) ** self.ss) - else: - warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + - f"The resulting Displace object will retain the steady-state value of the original Displace object.") - return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=self.ss) - - -class AccumulatedDerivative: - """A container for accumulated derivative information to help calculate the sequence space Jacobian - of the outputs of a SimpleBlock with respect to its inputs. - Uses common (i, m) -> x notation as in SimpleSparse (see its docs for more details) as a sparse representation of - a Jacobian of outputs Y at any time t with respect to inputs X at any time s. - - Attributes: - `.elements`: `dict` - A mapping from tuples, (i, m), to floats, x, where i is the index of the non-zero diagonal - relative to the main diagonal (0), where m is the number of initial entries missing from the diagonal - (same conceptually as in SimpleSparse), and x is the value of the accumulated derivatives. - `.f_value`: `float` - The function value of the AccumulatedDerivative to be used when applying the chain rule in finding a subsequent - simple derivative. We can think of a SimpleBlock is a composition of simple functions - (either time displacements, arithmetic operators, etc.), i.e. f_i(f_{i-1}(...f_2(f_1(y))...)), where - at each step i as we are accumulating the derivatives through each simple function, if the derivative of any - f_i requires the chain rule, we will need the function value of the previous f_{i-1} to calculate that derivative. - `._keys`: `list` - The keys from the `.elements` attribute for convenience. - `._fp_values`: `list` - The values from the `.elements` attribute for convenience. `_fp_values` stands for f prime values, i.e. the actual - values of the accumulated derivative themselves. - """ - - def __init__(self, elements={(0, 0): 1.}, f_value=1.): - self.elements = elements - self.f_value = f_value - self._keys = list(self.elements.keys()) - self._fp_values = np.fromiter(self.elements.values(), dtype=float) - - @property - def ss(self): - return ignore(self.f_value) - - def __repr__(self): - formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' - return f'AccumulatedDerivative({formatted})' - - # TODO: Rewrite this comment for clarity once confirmed that the paper's notation will change - # (i, m)/(j, n) correspond to the Q_(-i, m), Q_(-j, n) operators defined for - # Proposition 2 of the Sequence Space Jacobian paper. - # The flipped sign in the code is so that the index 'i' matches the k(i) notation - # for writing SimpleBlock functions. Thus, it follows the same convention as SimpleSparse. - # Also because __call__ on a AccumulatedDerivative is a simple shift operator, it will take the form - # Q_(-i, 0) being applied to Q_(-j, n) (following the notation in the paper) - # s.t. Q_(-i, 0) Q_(-j, n) = Q(k,l) - def __call__(self, i): - keys = [(i + j, compute_l(-i, 0, -j, n)) for j, n in self._keys] - return AccumulatedDerivative(elements=dict(zip(keys, self._fp_values)), f_value=self.f_value) - - def apply(self, f, h=1e-5, **kwargs): - if f == np.log: - return AccumulatedDerivative(elements=dict(zip(self._keys, - [1 / self.f_value * x for x in self._fp_values])), - f_value=np.log(self.f_value)) - else: - return AccumulatedDerivative(elements=dict(zip(self._keys, [(f(self.f_value + h, **kwargs) - - f(self.f_value - h, **kwargs)) / (2 * h) * x - for x in self._fp_values])), - f_value=f(self.f_value, **kwargs)) - - def __pos__(self): - return AccumulatedDerivative(elements=dict(zip(self._keys, +self._fp_values)), f_value=+self.f_value) - - def __neg__(self): - return AccumulatedDerivative(elements=dict(zip(self._keys, -self._fp_values)), f_value=-self.f_value) - - def __add__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), - f_value=self.f_value + numeric_primitive(other)) - elif isinstance(other, AccumulatedDerivative): - elements = self.elements.copy() - for im, x in other.elements.items(): - if im in elements: - elements[im] += x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = x - - return AccumulatedDerivative(elements=elements, f_value=self.f_value + other.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __radd__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), - f_value=numeric_primitive(other) + self.f_value) - elif isinstance(other, AccumulatedDerivative): - elements = other.elements.copy() - for im, x in self.elements.items(): - if im in elements: - elements[im] += x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = x - - return AccumulatedDerivative(elements=elements, f_value=other.f_value + self.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __sub__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), - f_value=self.f_value - numeric_primitive(other)) - elif isinstance(other, AccumulatedDerivative): - elements = self.elements.copy() - for im, x in other.elements.items(): - if im in elements: - elements[im] -= x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = -x - - return AccumulatedDerivative(elements=elements, f_value=self.f_value - other.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __rsub__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, -self._fp_values)), - f_value=numeric_primitive(other) - self.f_value) - elif isinstance(other, AccumulatedDerivative): - elements = other.elements.copy() - for im, x in self.elements.items(): - if im in elements: - elements[im] -= x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = -x - - return AccumulatedDerivative(elements=elements, f_value=other.f_value - self.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __mul__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values * numeric_primitive(other))), - f_value=self.f_value * numeric_primitive(other)) - elif isinstance(other, AccumulatedDerivative): - return AccumulatedDerivative(elements=(self * other.f_value + other * self.f_value).elements, - f_value=self.f_value * other.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __rmul__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, numeric_primitive(other) * self._fp_values)), - f_value=numeric_primitive(other) * self.f_value) - elif isinstance(other, AccumulatedDerivative): - return AccumulatedDerivative(elements=(other * self.f_value + self * other.f_value).elements, - f_value=other.f_value * self.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __truediv__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values / numeric_primitive(other))), - f_value=self.f_value / numeric_primitive(other)) - elif isinstance(other, AccumulatedDerivative): - return AccumulatedDerivative(elements=((other.f_value * self - self.f_value * other) / - (other.f_value ** 2)).elements, - f_value=self.f_value / other.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __rtruediv__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, -numeric_primitive(other) / - self.f_value ** 2 * self._fp_values)), - f_value=numeric_primitive(other) / self.f_value) - elif isinstance(other, AccumulatedDerivative): - return AccumulatedDerivative(elements=((self.f_value * other - other.f_value * self) / - (self.f_value ** 2)).elements, f_value=other.f_value / self.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __pow__(self, power, modulo=None): - if np.isscalar(power): - return AccumulatedDerivative(elements=dict(zip(self._keys, numeric_primitive(power) * self.f_value - ** numeric_primitive(power - 1) * self._fp_values)), - f_value=self.f_value ** numeric_primitive(power)) - elif isinstance(power, AccumulatedDerivative): - return AccumulatedDerivative(elements=(self.f_value ** (power.f_value - 1) * ( - power.f_value * self + power * self.f_value * np.log(self.f_value))).elements, - f_value=self.f_value ** power.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - def __rpow__(self, other): - if np.isscalar(other): - return AccumulatedDerivative(elements=dict(zip(self._keys, np.log(other) * numeric_primitive(other) ** - self.f_value * self._fp_values)), - f_value=numeric_primitive(other) ** self.f_value) - elif isinstance(other, AccumulatedDerivative): - return AccumulatedDerivative(elements=(other.f_value ** (self.f_value - 1) * ( - self.f_value * other + self * other.f_value * np.log(other.f_value))).elements, - f_value=other.f_value ** self.f_value) - else: - raise NotImplementedError("This operation is not yet supported for non-scalar arguments") - - -def compute_l(i, m, j, n): - """Computes the `l` index from the composition of shift operators, Q_{i, m} Q_{j, n} = Q_{k, l} in Proposition 2 - of the paper (regarding efficient multiplication of simple Jacobians).""" - if i >= 0 and j >= 0: - return max(m - j, n) - elif i >= 0 and j <= 0: - return max(m, n) + min(i, -j) - elif i <= 0 and j >= 0 and i + j >= 0: - return max(m - i - j, n) - elif i <= 0 and j >= 0 and i + j <= 0: - return max(n + i + j, m) - else: - return max(m, n + i) - - -# TODO: This needs its own unit test -def vectorize_func_over_time(func, *args): - """In `args` some arguments will be Displace objects and others will be Ignore/IgnoreVector objects. - The Displace objects will have an extra time dimension (as its first dimension). - We need to ensure that `func` is evaluated at the non-time dependent steady-state value of - the Ignore/IgnoreVectors and at each of the time-dependent values, t, of the Displace objects or in other - words along its time path. - """ - d_inds = [i for i in range(len(args)) if isinstance(args[i], Displace)] - x_path = [] - - # np.shape(args[d_inds[0]])[0] is T, the size of the first dimension of the first Displace object - # provided in args (assume all Displaces are the same shape s.t. they're conformable) - for t in range(np.shape(args[d_inds[0]])[0]): - x_path.append(func(*[args[i][t] if i in d_inds else args[i] for i in range(len(args))])) - - return np.array(x_path) - - -def apply_function(func, *args, **kwargs): - """Ensure that for generic functions called within a block and acting on a Displace object - properly instantiates the steady state value of the created Displace object""" - if np.any([isinstance(x, Displace) for x in args]): - x_path = vectorize_func_over_time(func, *args) - return Displace(x_path, ss=func(*[x.ss if isinstance(x, Displace) else numeric_primitive(x) for x in args])) - elif np.any([isinstance(x, AccumulatedDerivative) for x in args]): - raise NotImplementedError( - "Have not yet implemented general apply_function functionality for AccumulatedDerivatives") - else: - return func(*args, **kwargs) diff --git a/sequence_jacobian/devtools/__init__.py b/sequence_jacobian/devtools/__init__.py deleted file mode 100644 index 9795de6..0000000 --- a/sequence_jacobian/devtools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Tools for debugging, developing, and deprecating code""" - -from . import analysis, debug, deprecate, upgrade diff --git a/sequence_jacobian/devtools/analysis.py b/sequence_jacobian/devtools/analysis.py deleted file mode 100644 index 77e90d8..0000000 --- a/sequence_jacobian/devtools/analysis.py +++ /dev/null @@ -1,242 +0,0 @@ -"""Low-level tools/classes for analyzing sequence-jacobian model DAGs to support debugging""" - -import numpy as np -import xarray as xr -from collections.abc import Iterable - -from ..utilities import graph - - -class BlockIONetwork: - """ - A 3-d axis-labeled DataArray (blocks x inputs x outputs), which allows for the analysis of the input-output - structure of a DAG. - """ - def __init__(self, blocks, calibration=None, ignore_helpers=True): - topsorted, inset, outset = graph.block_sort(blocks, return_io=True, calibration=calibration, - ignore_helpers=ignore_helpers) - self.blocks = {b.name: b for b in blocks} - self.blocks_names = list(self.blocks.keys()) - self.blocks_as_list = list(self.blocks.values()) - self.var_names = list(inset.union(outset)) - self.darray = xr.DataArray(np.zeros((len(blocks), len(self.var_names), len(self.var_names))), - coords=[self.blocks_names, self.var_names, self.var_names], - dims=["blocks", "inputs", "outputs"]) - - def __repr__(self): - return self.darray.__repr__() - - # User-facing "print" methods - def print_block_links(self, block_name): - print(f" Links in {block_name}") - print(" " + "-" * (len(f" Links in {block_name}"))) - - link_inds = np.nonzero(self._subset_by_block(block_name)).data - links = [] - for i in range(np.shape(link_inds)[0]): - i_ind, o_ind = link_inds[:, i] - i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) - o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) - links.append([i_var, o_var]) - self._print_links(links) - - def print_all_var_links(self, var_name, calibration=None, ignore_helpers=True): - print(f" Links from {var_name}") - print(" " + "-" * (len(f" Links for {var_name}"))) - - links = self.find_all_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) - self._print_links(links) - - @staticmethod - def _print_links(links): - link_strs = [] - # Create " -> " linked strings and sort them for nicer printing - for link in links: - link_strs.append(" " + " -> ".join(link)) - link_strs.sort() - for link_str in link_strs: - print(link_str) - print("") # To break lines - - def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): - print(f" Links between {unknowns} and {targets}") - print(" " + "-" * (len(f" Links between {unknowns} and {targets}"))) - unknown_target_net = self.find_unknowns_targets_links(unknowns, targets, calibration=calibration, - ignore_helpers=ignore_helpers) - print(unknown_target_net) - print("") # To break lines - - # TODO: Implement an enhancement to display the "closest" link if missing. - def query_var_link(self, input_var, output_var, calibration=None, ignore_helpers=True): - all_links = self.find_all_var_links(input_var, calibration=calibration, ignore_helpers=ignore_helpers) - link_paths = [] - for link in all_links: - if link[0] == input_var and link[-1] == output_var: - link_paths.append(link) - if link_paths: - print(f" Links between {input_var} and {output_var}") - print(" " + "-" * (len(f" Links between {input_var} and {output_var}"))) - self._print_links(link_paths) - else: - print(f"There are no links within the DAG connecting {input_var} to {output_var}") - - # User-facing "analysis" methods - def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, - calibration=None, ignore_helpers=True): - """ - Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG - - Parameters - ---------- - inputs_to_be_recorded: `list(str)` - A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork - block_input_args: `dict` - A dict of variable/parameter names and values (typically from the steady state of the model) on which, - a block can perform a valid evaluation - calibration: `dict` or None - Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies - ignore_helpers: bool - Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies - """ - block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, ignore_helpers=ignore_helpers) - for input_var in inputs_to_be_recorded: - all_input_vars = set(input_var) - for ib in block_inds_sorted: - ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} - # This extra step is needed because some arguments required for calling .jac on - # HetBlock and SolvedBlock are not a part of .inputs - ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) - io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) - if io_links: - self._record_io_links(self.blocks_names[ib], io_links) - # Need to also track the paths of outputs which could be intermediate inputs further down the DAG - all_input_vars = all_input_vars.union(set(io_links.keys())) - - def find_all_var_links(self, var_name, calibration=None, ignore_helpers=True): - # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those - # `outputs` and instantiate the initial list of links - link_inds = np.nonzero(self._subset_by_vars(var_name).data) - links = [[var_name] for i in range(len(link_inds[0]))] - - block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, - ignore_helpers=ignore_helpers) - required = graph.find_outputs_that_are_intermediate_inputs(self.blocks_as_list, ignore_helpers=ignore_helpers) - intermediates = set() - for ib in block_inds_sorted: - # Note: This block is ordered before the bottom block of code since the intermediate outputs from a block - # `ib` do not need to be checked as inputs to that same block `ib`, only the subsequent blocks - if intermediates: - intm_link_inds = np.nonzero(self._subset_by_vars(list(intermediates)).data) - # If there are `intermediate` inputs that have been recorded, we need to find the *indirect* links - for iil, iilb in enumerate(intm_link_inds[0]): - # Check if those inputs are inputs to block `ib` - if ib == iilb: - # If so, repeat the logic from below, where you find the input-output var link - o_var = str(self._subset_by_vars(list(intermediates)).coords["outputs"][intm_link_inds[2][iil]].data) - intm_i_var = str(self._subset_by_vars(list(intermediates)).coords["inputs"][intm_link_inds[1][iil]].data) - - # And add it to the set of all links, recording this new links' output if it hasn't appeared - # before and if it is an intermediate input - links.append([intm_i_var, o_var]) - if o_var in required: - intermediates = intermediates.union([o_var]) - - # Check if `var_name` enters into that block as an input, and if so add the link between it and the output - # it directly affects, recording that output as an intermediate input if needed - # Note: link_inds' 0-th row indicates the blocks and 1st row indicates the outputs - for il, ilb in enumerate(link_inds[0]): - if ib == ilb: - o_var = str(self._subset_by_vars(var_name).coords["outputs"][link_inds[1][il]].data) - links[il].append(o_var) - if o_var in required: - intermediates = intermediates.union([o_var]) - return _compose_dyad_links(links) - - def find_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): - unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), - coords=[unknowns, targets], - dims=["inputs", "outputs"]) - for u in unknowns: - links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) - for link in links: - if link[0] == u and link[-1] in targets: - unknown_target_net.loc[u, link[-1]] = 1. - return unknown_target_net - - # Analysis support methods - def _subset_by_block(self, block_name): - return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] - - def _subset_by_vars(self, vars_names): - if isinstance(vars_names, Iterable): - return self.darray.loc[[b.name for b in self.blocks.values() if np.any(v in b.inputs for v in vars_names)], - vars_names, :] - else: - return self.darray.loc[[b.name for b in self.blocks.values() if vars_names in b.inputs], vars_names, :] - - def _record_io_links(self, block_name, io_links): - for o, i in io_links.items(): - self.darray.loc[block_name, i, o] = 1. - - -def _compose_dyad_links(links): - links_composed = [] - inds_to_ignore = set() - outputs = set() - - for il, link in enumerate(links): - if il in inds_to_ignore: - continue - if links_composed: - if link[0] in outputs: - # Since `link` has as its input one of the outputs recorded from prior links in `links_composed` - # search through the links in `links_composed` to see which links need to be extended with `link` - # and the other links with the same input as `link` - link_extensions = [] - # Potential link extensions will only be located past the stage il that we are at - for il_e in range(il, len(links)): - if links[il_e][0] == link[0]: - link_extensions.append(links[il_e]) - outputs = outputs.union([links[il_e][-1]]) - inds_to_ignore = inds_to_ignore.union([il_e]) - - links_to_add = [] - inds_to_omit = [] - for il_c, link_c in enumerate(links_composed): - if link_c[-1] == link[0]: - inds_to_omit.append(il_c) - links_to_add.extend([link_c + [ext[-1]] for ext in link_extensions]) - - links_composed = [link_c for i, link_c in enumerate(links_composed) if i not in inds_to_omit] + links_to_add - else: - links_composed.append(link) - outputs = outputs.union([link[-1]]) - else: - links_composed.append(link) - outputs = outputs.union([link[-1]]) - return links_composed - - -def find_io_links(block, input_args, block_input_args): - """ - For a given `block`, see which output arguments the input argument `input_args` affects - - Parameters - ---------- - block: `Block` object - One of the various kinds of `Block` objects (`SimpleBlock`, `HetBlock`, etc.) - input_args: `str` or `list(str)` - The input arguments, whose paths through the block to the output variables we want to see - block_input_args: `dict{str: num}` - The rest of the input arguments required to evaluate the block's Jacobian - - Returns - ------- - links: `dict{str: list(str)}` - A dict with *output arguments* as keys, and the input arguments that affect it as values - """ - J = block.jac(ss=block_input_args, T=2, shock_list=input_args) - links = {} - for o in J.outputs: - links[o] = list(J.nesteddict[o].keys()) - return links diff --git a/sequence_jacobian/devtools/debug.py b/sequence_jacobian/devtools/debug.py deleted file mode 100644 index 48e22ec..0000000 --- a/sequence_jacobian/devtools/debug.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Top-level tools to help users debug their sequence-jacobian code""" - -import warnings -import numpy as np - -from . import analysis -from ..utilities import graph - - -def ensure_computability(blocks, calibration=None, unknowns_ss=None, - exogenous=None, unknowns=None, targets=None, ss=None, - verbose=False, fragile=True, ignore_helpers=True): - # Check if `calibration` and `unknowns` jointly have all of the required variables needed to be able - # to calculate a steady state. If ss is provided, assume the user doesn't need to check this - if calibration and unknowns_ss and not ss: - ensure_all_inputs_accounted_for(blocks, calibration, unknowns_ss, verbose=verbose, fragile=fragile) - - # Check if unknowns and exogenous are not outputs of any blocks, and that targets are not inputs to any blocks - if exogenous and unknowns and targets: - ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous + unknowns, targets, - verbose=verbose, fragile=fragile) - - # Check if there are any "broken" links between unknowns and targets, i.e. if there are any unknowns that don't - # affect any targets, or if there are any targets that aren't affected by any unknowns - if unknowns and targets and ss: - ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile) - - -# To ensure that no input argument that is required for one of the blocks to evaluate is missing -def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False, fragile=True): - variables_accounted_for = set(unknowns.keys()).union(set(calibration.keys())) - all_inputs = set().union(*[b.inputs for b in blocks]) - required = graph.find_outputs_that_are_intermediate_inputs(blocks) - non_computed_inputs = all_inputs.difference(required) - - variables_unaccounted_for = non_computed_inputs.difference(variables_accounted_for) - if variables_unaccounted_for: - if fragile: - raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" - f"parameters: {variables_unaccounted_for}") - else: - warnings.warn(f"\nThe following variables were not listed as unknowns or provided as fixed variables/" - f"parameters: {variables_unaccounted_for}") - if verbose: - print("This DAG accounts for all inputs variables.") - - -def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unknowns, targets, - verbose=False, fragile=True): - cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks) - invalid_xu = [] - invalid_targ = [] - for xu in exogenous_unknowns: - if xu not in cand_xu: - invalid_xu.append(xu) - for targ in targets: - if targ not in cand_targets: - invalid_targ.append(targ) - if invalid_xu or invalid_targ: - if fragile: - raise RuntimeError(f"The following exogenous/unknowns are invalid candidates: {invalid_xu}\n" - f"The following targets are invalid candidates: {invalid_targ}") - else: - warnings.warn(f"\nThe following exogenous/unknowns are invalid candidates: {invalid_xu}\n" - f"The following targets are invalid candidates: {invalid_targ}") - if verbose: - print("The provided exogenous/unknowns and targets are all valid candidates for this DAG.") - - -def find_candidate_unknowns_and_targets(block_list, verbose=False): - dep, inputs, outputs = graph.block_sort(block_list, return_io=True) - required = graph.find_outputs_that_are_intermediate_inputs(block_list) - - # Candidate exogenous and unknowns (also includes parameters): inputs that are not outputs of any block - # Candidate targets: outputs that are not inputs to any block - cand_xu = inputs.difference(required) - cand_targets = outputs.difference(required) - - if verbose: - print(f"Candidate exogenous/unknowns: {cand_xu}\n" - f"Candidate targets: {cand_targets}") - - return cand_xu, cand_targets - - -def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True): - io_net = analysis.BlockIONetwork(blocks) - io_net.record_input_variables_paths(unknowns, ss) - ut_net = io_net.find_unknowns_targets_links(unknowns, targets) - broken_unknowns = [] - broken_targets = [] - for u in unknowns: - if not np.any(ut_net.loc[u, :]): - broken_unknowns.append(u) - for t in targets: - if not np.any(ut_net.loc[:, t]): - broken_targets.append(t) - if broken_unknowns or broken_targets: - if fragile: - raise RuntimeError(f"The following unknowns don't affect any targets: {broken_unknowns}\n" - f"The following targets aren't affected by any unknowns: {broken_targets}") - else: - warnings.warn(f"\nThe following unknowns don't affect any targets: {broken_unknowns}\n" - f"The following targets aren't affected by any unknowns: {broken_targets}") - if verbose: - print("This DAG does not contain any broken links between unknowns and targets.") diff --git a/sequence_jacobian/devtools/deprecate.py b/sequence_jacobian/devtools/deprecate.py deleted file mode 100644 index e7b4cb1..0000000 --- a/sequence_jacobian/devtools/deprecate.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tools for deprecating older SSJ code conventions in favor of newer conventions""" - -import warnings - -# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, by temporarily -# providing support for old conventions via deprecated methods, providing time to allow for a seamless upgrade -# to newer versions sequence-jacobian. - -# TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions -# themselves. - - -def rename_output_list_to_outputs(outputs=None, output_list=None): - if output_list is not None: - warnings.warn("The output_list kwarg has been deprecated and replaced with the outputs kwarg.", - DeprecationWarning) - if outputs is None: - return output_list - else: - return list(set(outputs) | set(output_list)) - else: - return outputs diff --git a/sequence_jacobian/devtools/upgrade.py b/sequence_jacobian/devtools/upgrade.py deleted file mode 100644 index e2994d8..0000000 --- a/sequence_jacobian/devtools/upgrade.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Tools for upgrading from older SSJ code conventions""" - -# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, and who -# may want additional support/tools for ensuring that their attempts to upgrade to use newer versions of -# sequence-jacobian has been successfully. - -import numpy as np - - -def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): - """ - This code is meant to provide a quick comparison of `ss_ref` the reference steady state dict from old code, and - `ss_comp` the steady state computed from the newer code. - """ - if name_map is None: - name_map = {} - - # Compare the steady state values present in both ss_ref and ss_comp - for key_ref in ss_ref.keys(): - if key_ref in ss_comp.keys(): - key_comp = key_ref - elif key_ref in name_map: - key_comp = name_map[key_ref] - else: - continue - if verbose: - if np.isscalar(ss_ref[key_ref]): - print(f"{key_ref} resid: {abs(ss_ref[key_ref] - ss_comp[key_comp])}") - else: - print(f"{key_ref} resid: {np.linalg.norm(ss_ref[key_ref].flatten() - ss_comp[key_comp].flatten(), np.inf)}") - else: - assert np.isclose(ss_ref[key_ref], ss_comp[key_comp]) - - # Show the steady state values present in only one of ss_ref or ss_comp - ss_ref_incl_mapped = set(ss_ref.keys()) - set(name_map.keys()) - ss_comp_incl_mapped = set(ss_comp.keys()) - set(name_map.values()) - diff_keys = ss_ref_incl_mapped.symmetric_difference(ss_comp_incl_mapped) - if diff_keys: - print(f"The keys present in only one of the two steady state dicts are {diff_keys}") diff --git a/sequence_jacobian/estimation.py b/sequence_jacobian/estimation.py deleted file mode 100644 index a633640..0000000 --- a/sequence_jacobian/estimation.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Functions for calculating the log likelihood of a model from its impulse responses""" - -import numpy as np -import scipy.linalg as linalg -from numba import njit - -'''Part 1: compute covariances at all lags and log likelihood''' - - -def all_covariances(M, sigmas): - """Use Fast Fourier Transform to compute covariance function between O vars up to T-1 lags. - - See equation (108) in appendix B.5 of paper for details. - - Parameters - ---------- - M : array (T*O*Z), stacked impulse responses of nO variables to nZ shocks (MA(T-1) representation) - sigmas : array (Z), standard deviations of shocks - - Returns - ---------- - Sigma : array (T*O*O), covariance function between O variables for 0, ..., T-1 lags - """ - T = M.shape[0] - dft = np.fft.rfftn(M, s=(2 * T - 2,), axes=(0,)) - total = (dft.conjugate() * sigmas**2) @ dft.swapaxes(1, 2) - return np.fft.irfftn(total, s=(2 * T - 2,), axes=(0,))[:T] - - -def log_likelihood(Y, Sigma, sigma_measurement=None): - """Given second moments, compute log-likelihood of data Y. - - Parameters - ---------- - Y : array (Tobs*O) - stacked data for O observables over Tobs periods - Sigma : array (T*O*O) - covariance between observables in model for 0, ... , T lags (e.g. from all_covariances) - sigma_measurement : [optional] array (O) - std of measurement error for each observable, assumed zero if not provided - - Returns - ---------- - L : scalar, log-likelihood - """ - Tobs, nO = Y.shape - if sigma_measurement is None: - sigma_measurement = np.zeros(nO) - V = build_full_covariance_matrix(Sigma, sigma_measurement, Tobs) - y = Y.ravel() - return log_likelihood_formula(y, V) - - -'''Part 2: helper functions''' - - -def log_likelihood_formula(y, V): - """Implements multivariate normal log-likelihood formula using Cholesky with data vector y and variance V. - Calculates -log det(V)/2 - y'V^(-1)y/2 - """ - V_factored = linalg.cho_factor(V) - quadratic_form = np.dot(y, linalg.cho_solve(V_factored, y)) - log_determinant = 2*np.sum(np.log(np.diag(V_factored[0]))) - return -(log_determinant + quadratic_form) / 2 - - -@njit -def build_full_covariance_matrix(Sigma, sigma_measurement, Tobs): - """Takes in T*O*O array Sigma with covariances at each lag t, - assembles them into (Tobs*O)*(Tobs*O) matrix of covariances, including measurement errors. - """ - T, O, O = Sigma.shape - V = np.empty((Tobs, O, Tobs, O)) - for t1 in range(Tobs): - for t2 in range(Tobs): - if abs(t1-t2) >= T: - V[t1, :, t2, :] = np.zeros((O, O)) - else: - if t1 < t2: - V[t1, : , t2, :] = Sigma[t2-t1, :, :] - elif t1 > t2: - V[t1, : , t2, :] = Sigma[t1-t2, :, :].T - else: - # want exactly symmetric - V[t1, :, t2, :] = (np.diag(sigma_measurement**2) + (Sigma[0, :, :]+Sigma[0, :, :].T)/2) - return V.reshape((Tobs*O, Tobs*O)) diff --git a/sequence_jacobian/jacobian/__init__.py b/sequence_jacobian/jacobian/__init__.py deleted file mode 100644 index 57070a8..0000000 --- a/sequence_jacobian/jacobian/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Jacobian computation and support functions""" diff --git a/sequence_jacobian/jacobian/classes.py b/sequence_jacobian/jacobian/classes.py deleted file mode 100644 index b522f7d..0000000 --- a/sequence_jacobian/jacobian/classes.py +++ /dev/null @@ -1,471 +0,0 @@ -"""Various classes to support the computation of Jacobians""" - -from abc import ABCMeta -import copy -import numpy as np - -from . import support - - -class Jacobian(metaclass=ABCMeta): - """An abstract base class encompassing all valid types representing Jacobians, which include - np.ndarray, IdentityMatrix, ZeroMatrix, and SimpleSparse.""" - pass - -# Make np.ndarray a child class of Jacobian -Jacobian.register(np.ndarray) - - -class IdentityMatrix(Jacobian): - """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, - use to initialize Jacobian of a variable wrt itself""" - __array_priority__ = 10_000 - - def sparse(self): - """Equivalent SimpleSparse representation, less efficient operations but more general.""" - return SimpleSparse({(0, 0): 1}) - - def matrix(self, T): - return np.eye(T) - - def __matmul__(self, other): - """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" - return copy.deepcopy(other) - - def __rmatmul__(self, other): - return copy.deepcopy(other) - - def __mul__(self, a): - return a*self.sparse() - - def __rmul__(self, a): - return self.sparse()*a - - def __add__(self, x): - return self.sparse() + x - - def __radd__(self, x): - return x + self.sparse() - - def __sub__(self, x): - return self.sparse() - x - - def __rsub__(self, x): - return x - self.sparse() - - def __neg__(self): - return -self.sparse() - - def __pos__(self): - return self - - def __repr__(self): - return 'IdentityMatrix' - - -class ZeroMatrix(Jacobian): - """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, - use in common case where some outputs don't depend on inputs""" - __array_priority__ = 10_000 - - def sparse(self): - return SimpleSparse({(0, 0): 0}) - - def matrix(self, T): - return np.zeros((T,T)) - - def __matmul__(self, other): - if isinstance(other, np.ndarray) and other.ndim == 1: - return np.zeros_like(other) - else: - return self - - def __rmatmul__(self, other): - return self @ other - - def __mul__(self, a): - return self - - def __rmul__(self, a): - return self - - # copies seem inefficient here, try to live without them - def __add__(self, x): - return x - - def __radd__(self, x): - return x - - def __sub__(self, x): - return -x - - def __rsub__(self, x): - return x - - def __neg__(self): - return self - - def __pos__(self): - return self - - def __repr__(self): - return 'ZeroMatrix' - - -class SimpleSparse(Jacobian): - """Efficient representation of sparse linear operators, which are linear combinations of basis - operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s - (measured by # above main diagonal) and m is number of initial entries missing. - - Examples of such basis operators: - - (0, 0) is identity operator - - (0, 2) is identity operator with first two '1's on main diagonal missing - - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator - - (-1, 1) has 1s on diagonal below main diagonal, except first column - - The linear combination of these basis operators that makes up a given SimpleSparse object is - stored as a dict 'elements' mapping (i, m) -> x. - - The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need - the more general basis (i, m) to ensure closure under multiplication. - - These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space - Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation - for writing SimpleBlock functions. - - The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix - operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less - interchangeably with ordinary NumPy matrices. - """ - - # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules - __array_priority__ = 1000 - - def __init__(self, elements): - self.elements = elements - self.indices, self.xs = None, None - - @staticmethod - def from_simple_diagonals(elements): - """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" - return SimpleSparse({(i, 0): x for i, x in elements.items()}) - - def matrix(self, T): - """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" - return self + np.zeros((T, T)) - - def array(self): - """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) - and one size-N array of floats with entries x. - - This is needed for Numba to take as input. Cache for efficiency. - """ - if self.indices is not None: - return self.indices, self.xs - else: - indices, xs = zip(*self.elements.items()) - self.indices, self.xs = np.array(indices), np.array(xs) - return self.indices, self.xs - - @property - def T(self): - """Transpose""" - return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) - - @property - def iszero(self): - return not self.nonzero().elements - - def nonzero(self): - elements = self.elements.copy() - for im, x in self.elements.items(): - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - return SimpleSparse(elements) - - def __pos__(self): - return self - - def __neg__(self): - return SimpleSparse({im: -x for im, x in self.elements.items()}) - - def __matmul__(self, A): - if isinstance(A, SimpleSparse): - # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs - return SimpleSparse(support.multiply_rs_rs(self, A)) - elif isinstance(A, np.ndarray): - # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing - indices, xs = self.array() - if A.ndim == 2: - return support.multiply_rs_matrix(indices, xs, A) - elif A.ndim == 1: - return support.multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] - else: - return NotImplemented - else: - return NotImplemented - - def __rmatmul__(self, A): - # multiplication rule when this object is on right (will only be called when left is matrix) - # for simplicity, just use transpose to reduce this to previous cases - return (self.T @ A.T).T - - def __add__(self, A): - if isinstance(A, SimpleSparse): - # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap - elements = self.elements.copy() - for im, x in A.elements.items(): - if im in elements: - elements[im] += x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = x - return SimpleSparse(elements) - else: - # add SimpleSparse to T*T matrix - if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: - return NotImplemented - T = A.shape[0] - - # fancy trick to do this efficiently by writing A as flat vector - # then (i, m) can be mapped directly to NumPy slicing! - A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy - for (i, m), x in self.elements.items(): - if i < 0: - A[T * (-i) + (T + 1) * m::T + 1] += x - else: - A[i + (T + 1) * m:(T - i) * T:T + 1] += x - return A.reshape((T, T)) - - def __radd__(self, A): - try: - return self + A - except: - print(self) - print(A) - raise - - def __sub__(self, A): - # slightly inefficient implementation with temporary for simplicity - return self + (-A) - - def __rsub__(self, A): - return -self + A - - def __mul__(self, a): - if not np.isscalar(a): - return NotImplemented - return SimpleSparse({im: a * x for im, x in self.elements.items()}) - - def __rmul__(self, a): - return self * a - - def __repr__(self): - formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' - return f'SimpleSparse({formatted})' - - def __eq__(self, s): - return self.elements == s.elements - - -class NestedDict: - def __init__(self, nesteddict, outputs=None, inputs=None, name=None): - if isinstance(nesteddict, NestedDict): - self.nesteddict = nesteddict.nesteddict - self.outputs = nesteddict.outputs - self.inputs = nesteddict.inputs - self.name = nesteddict.name - else: - self.nesteddict = nesteddict - if outputs is None: - outputs = list(nesteddict.keys()) - if inputs is None: - inputs = [] - for v in nesteddict.values(): - inputs.extend(list(v)) - inputs = deduplicate(inputs) - - self.outputs = list(outputs) - self.inputs = list(inputs) - if name is None: - # TODO: Figure out better default naming scheme for NestedDicts - self.name = "NestedDict" - else: - self.name = name - - def __repr__(self): - return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' - - def __iter__(self): - return iter(self.outputs) - - def __or__(self, other): - # non-in-place merge: make a copy, then update - merged = type(self)(self.nesteddict, self.outputs, self.inputs) - merged.update(other) - return merged - - def __getitem__(self, x): - if isinstance(x, str): - # case 1: just a single output, give subdict - return self.nesteddict[x] - elif isinstance(x, tuple): - # case 2: tuple, referring to output and input - o, i = x - o = self.outputs if o == slice(None, None, None) else o - i = self.inputs if i == slice(None, None, None) else i - if isinstance(o, str): - if isinstance(i, str): - # case 2a: one output, one input, return single Jacobian - return self.nesteddict[o][i] - else: - # case 2b: one output, multiple inputs, return dict - return {ii: self.nesteddict[o][ii] for ii in i} - else: - # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i - i = (i,) if isinstance(i, str) else i - return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) - elif isinstance(x, list) or isinstance(x, set): - # case 3: assume that list or set refers just to outputs, get all of those - return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) - else: - raise ValueError(f'Tried to get impermissible item {x}') - - def get(self, *args, **kwargs): - # this is for compatibility, not a huge fan - return self.nesteddict.get(*args, **kwargs) - - def update(self, J): - if set(self.inputs) != set(J.inputs): - raise ValueError \ - (f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') - if not set(self.outputs).isdisjoint(J.outputs): - raise ValueError \ - (f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') - self.outputs = self.outputs + J.outputs - self.nesteddict = {**self.nesteddict, **J.nesteddict} - - # Ensure that every output in self has either a Jacobian or filler value for each input, - # s.t. all inputs map to all outputs - def complete(self, filler): - nesteddict = {} - for o in self.outputs: - nesteddict[o] = dict(self.nesteddict[o]) - for i in self.inputs: - if i not in nesteddict[o]: - nesteddict[o][i] = filler - return type(self)(nesteddict, self.outputs, self.inputs) - - -def deduplicate(mylist): - """Remove duplicates while otherwise maintaining order""" - return list(dict.fromkeys(mylist)) - - -class JacobianDict(NestedDict): - def __init__(self, nesteddict, outputs=None, inputs=None, name=None): - ensure_valid_jacobiandict(nesteddict) - super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) - - @staticmethod - def identity(ks): - return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() - - def complete(self): - return super().complete(ZeroMatrix()) - - def addinputs(self): - """Add any inputs that were not already in output list as outputs, with the identity""" - inputs = [x for x in self.inputs if x not in self.outputs] - return self | JacobianDict.identity(inputs) - - def __matmul__(self, x): - if isinstance(x, JacobianDict): - return self.compose(x) - else: - return self.apply(x) - - def __bool__(self): - return bool(self.outputs) and bool(self.inputs) - - def compose(self, J): - o_list = self.outputs - m_list = tuple(set(self.inputs) & set(J.outputs)) - i_list = J.inputs - - J_om = self.complete().nesteddict - J_mi = J.complete().nesteddict - J_oi = {} - - for o in o_list: - J_oi[o] = {} - for i in i_list: - Jout = ZeroMatrix() - for m in m_list: - J_om[o][m] - J_mi[m][i] - Jout += J_om[o][m] @ J_mi[m][i] - J_oi[o][i] = Jout - - return JacobianDict(J_oi, o_list, i_list) - - def apply(self, x): - # assume that all entries in x have some length T, and infer it - T = len(next(iter(x.values()))) - - inputs = x.keys() & set(self.inputs) - J_oi = self.complete().nesteddict - y = {} - - for o in self.outputs: - y[o] = np.zeros(T) - for i in inputs: - y[o] += J_oi[o][i] @ x[i] - - return y - - def pack(self, T): - J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) - for iO, O in enumerate(self.outputs): - for iI, I in enumerate(self.inputs): - J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = support.make_matrix(self[O, I], T) - return J - - @staticmethod - def unpack(bigjac, outputs, inputs, T): - """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" - jacdict = {} - for iO, O in enumerate(outputs): - jacdict[O] = {} - for iI, I in enumerate(inputs): - jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] - return JacobianDict(jacdict, outputs, inputs) - - -def ensure_valid_jacobiandict(d): - """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a - Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed - to be {}, which is permitted the empty version of a valid nested dict.""" - - if d != {} and not isinstance(d, JacobianDict): - # Assume it's sufficient to just check one of the keys - if not isinstance(next(iter(d.keys())), str): - raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") - - jac_o_dict = next(iter(d.values())) - if isinstance(jac_o_dict, dict): - if not isinstance(next(iter(jac_o_dict.keys())), str): - raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" - f" `input` names.") - jac_o_i = next(iter(jac_o_dict.values())) - if not isinstance(jac_o_i, Jacobian): - raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") - else: - if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: - raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") - else: - raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" - f" values of type `Jacobian`.") diff --git a/sequence_jacobian/jacobian/drivers.py b/sequence_jacobian/jacobian/drivers.py deleted file mode 100644 index 830554b..0000000 --- a/sequence_jacobian/jacobian/drivers.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Main methods (drivers) for computing and manipulating both block-level and model-level Jacobians""" - -import numpy as np - -from .classes import JacobianDict -from .support import pack_vectors, unpack_vectors -from ..utilities import misc, graph - -'''Drivers: - - get_H_U : get H_U matrix mapping all unknowns to all targets - - get_impulse : get single GE impulse response - - get_G : get G matrices characterizing all GE impulse responses - - - curlyJs_sorted : get block Jacobians curlyJ and return them topologically sorted - - forward_accumulate : forward accumulation on DAG, taking in topologically sorted Jacobians -''' - - -def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): - """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. - - Parameters - ---------- - blocks : list, simple blocks, het blocks, or jacdicts - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : int, truncation horizon - (if asymptotic, truncation horizon for backward iteration in HetBlocks) - ss : [optional] dict, steady state required if blocks contains any non-jacdicts - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - H_U : - if asymptotic=False: - array(T*n_u*T*n_u) H_U, Jacobian mapping all unknowns to all targets - is asymptotic=True: - array((2*Tpost-1)*n_u*n_u), representation of asymptotic columns of H_U - """ - - # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, Js) - - # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) - H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) - - # pack these n_u^2 matrices, each T*T, into a single matrix - return H_U_unpacked[targets, unknowns].pack(T) - - -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js=None): - """Get a single general equilibrium impulse response. - - Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed - and supplied to this function. Less so but still faster when H_U already computed. - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - dZ : dict, path of an exogenous variable - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if blocks contains non-jacdicts - outputs : [optional] list of str, variables we want impulse responses for - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - out : dict, impulse responses to shock dZ - """ - # step 0 (preliminaries): infer T, do topological sort and get curlyJs - if T is None: - for x in dZ.values(): - T = len(x) - break - - curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, Js) - - # step 1: do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) - H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) - - # step 2: do (vector) forward accumulation to get J^(o, curlyZ)dZ for all o in - # 'alloutputs', the combination of outputs (if specified) and targets - alloutputs = None - if outputs is not None: - alloutputs = set(outputs) | set(targets) - - J_curlyZ_dZ = forward_accumulate(curlyJs, dZ, alloutputs, required) - - # step 3: solve H_UdU = -H_ZdZ for dU - H_U = H_U_unpacked[targets, unknowns].pack(T) - H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) - dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) - dU = unpack_vectors(dU_packed, unknowns, T) - - # step 4: do (vector) forward accumulation to get J^(o, curlyU)dU - # then sum together with J^(o, curlyZ)dZ to get all output impulse responses - J_curlyU_dU = forward_accumulate(curlyJs, dU, outputs, required) - if outputs is None: - outputs = J_curlyZ_dZ.keys() | J_curlyU_dU.keys() - return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} - - -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js=None): - """Compute Jacobians G that fully characterize general equilibrium outputs in response - to all exogenous shocks in 'exogenous' - - Faster when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed - and supplied to this function. Less so but still faster when H_U already computed. - Relative benefit of precomputing these not as extreme as for get_impulse, since - obtaining and solving with H_U is a less dominant component of cost for getting Gs. - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - exogenous : list of str, names of exogenous shocks in DAG - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if blocks contains non-jacdicts - outputs : [optional] list of str, variables we want impulse responses for - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - G : dict of dict, Jacobians for general equilibrium mapping from exogenous to outputs - """ - - # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, Js) - - # step 2: do (matrix) forward accumulation to get - # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) - J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) - - # step 3: solve for G^U, unpack - H_U = J_curlyH_U[targets, unknowns].pack(T) - H_Z = J_curlyH_Z[targets, exogenous].pack(T) - - G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) - - # step 4: forward accumulation to get all outputs starting with G_U - # by default, don't calculate targets! - curlyJs = [G_U] + curlyJs - if outputs is None: - outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) - - -def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): - """ - Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs - and with respect to outputs of other blocks - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - inputs : list, input names we need to differentiate with respect to - ss : [optional] dict, steady state, needed if blocks includes blocks themselves - T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - curlyJs : list of dict of dict, curlyJ for each block in order of topological sort - required : list, outputs of some blocks that are needed as inputs by others - """ - - # step 1: get topological sort and required - topsorted = graph.block_sort(blocks) - required = graph.find_outputs_that_are_intermediate_inputs(blocks) - - # Remove any vector-valued outputs that are intermediate inputs, since we don't want - # to compute Jacobians with respect to vector-valued variables - if ss is not None: - vv_vars = set([k for k, v in ss.items() if np.size(v) > 1]) - required -= vv_vars - - # step 2: compute Jacobians and put them in right order - curlyJs = [] - shocks = set(inputs) | required - for num in topsorted: - block = blocks[num] - jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() - if k in misc.input_kwarg_list(block.jacobian)}) - - # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) - # then don't add it to the list of curlyJs to be returned - if not jac: - continue - else: - curlyJs.append(JacobianDict(jac)) - - return curlyJs, required - - -def forward_accumulate(curlyJs, inputs, outputs=None, required=None): - """ - Use forward accumulation on topologically sorted Jacobians in curlyJs to get - all cumulative Jacobians with respect to 'inputs' if inputs is a list of names, - or get outcome of apply to 'inputs' if inputs is dict. - - Optionally only find outputs in 'outputs', especially if we have knowledge of - what is required for later Jacobians. - - Note that the overloading of @ means that this works automatically whether curlyJs are ordinary - matrices, simple_block.SimpleSparse objects, or asymptotic.AsymptoticTimeInvariant objects, - as long as the first and third are not mixed (since multiplication not defined for them). - - Much-extended version of chain_jacobians. - - Parameters - ---------- - curlyJs : list of dict of dict, curlyJ for each block in order of topological sort - inputs : list or dict, input names to differentiate with respect to, OR dict of input vectors - outputs : [optional] list or set, outputs we're interested in - required : [optional] list or set, outputs needed for later curlyJs (only useful w/outputs) - - Returns - ------- - out : dict of dict or dict, either total J for each output wrt all inputs or - outcome from applying all curlyJs - """ - - if outputs is not None and required is not None: - # if list of outputs provided, we need to obtain these and 'required' along the way - alloutputs = set(outputs) | set(required) - else: - # otherwise, set to None, implies default behavior of obtaining all outputs in curlyJs - alloutputs = None - - # if inputs is list (jacflag=True), interpret as list of inputs for which we want to calculate jacs - # if inputs is dict, interpret as input *paths* to which we apply all Jacobians in curlyJs - jacflag = not isinstance(inputs, dict) - - if jacflag: - # Jacobians of inputs with respect to themselves are the identity, initialize with this - # out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} - out = JacobianDict.identity(inputs) - else: - out = inputs.copy() - - # iterate through curlyJs, in what is presumed to be a topologically sorted order - for curlyJ in curlyJs: - curlyJ = JacobianDict(curlyJ).complete() - if alloutputs is not None: - # if we want specific list of outputs, restrict curlyJ to that before continuing - curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] - if jacflag: - out.update(curlyJ.compose(out)) - else: - out.update(curlyJ.apply(out)) - - if outputs is not None: - # if we want specific list of outputs, restrict to that - # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) - return out[[k for k in outputs if k in out.outputs]] - else: - return out diff --git a/sequence_jacobian/jacobian/support.py b/sequence_jacobian/jacobian/support.py deleted file mode 100644 index 8ab3b76..0000000 --- a/sequence_jacobian/jacobian/support.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Various lower-level functions to support the computation of Jacobians""" - -import numpy as np -from numba import njit - - -# For supporting SimpleSparse -def multiply_basis(t1, t2): - """Matrix multiplication operation mapping two sparse basis elements to another.""" - # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with - # signs of i and j flipped to reflect different sign convention used here - i, m = t1 - j, n = t2 - k = i + j - if i >= 0: - if j >= 0: - l = max(m, n - i) - elif k >= 0: - l = max(m, n - k) - else: - l = max(m + k, n) - else: - if j <= 0: - l = max(m + j, n) - else: - l = max(m, n) + min(-i, j) - return k, l - - -def multiply_rs_rs(s1, s2): - """Matrix multiplication operation on two SimpleSparse objects.""" - # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, - # add all pairwise products to get overall product - elements = {} - for im, x in s1.elements.items(): - for jn, y in s2.elements.items(): - kl = multiply_basis(im, jn) - if kl in elements: - elements[kl] += x * y - else: - elements[kl] = x * y - return elements - - -@njit -def multiply_rs_matrix(indices, xs, A): - """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. - Much more computationally demanding than multiplying two SimpleSparse (which is almost - free with simple analytical formula), so we implement as jitted function.""" - n = indices.shape[0] - T = A.shape[0] - S = A.shape[1] - Aout = np.zeros((T, S)) - - for count in range(n): - # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' - # was stored in 'indices' and 'xs' - i = indices[count, 0] - m = indices[count, 1] - x = xs[count] - - # loop faster than vectorized when jitted - # directly use def of basis element (i, m), displacement of i and ignore first m - if i == 0: - for t in range(m, T): - for s in range(S): - Aout[t, s] += x * A[t, s] - elif i > 0: - for t in range(m, T - i): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - else: - for t in range(m - i, T): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - return Aout - - -def pack_vectors(vs, names, T): - v = np.zeros(len(names)*T) - for i, name in enumerate(names): - if name in vs: - v[i*T:(i+1)*T] = vs[name] - return v - - -def unpack_vectors(v, names, T): - vs = {} - for i, name in enumerate(names): - vs[name] = v[i*T:(i+1)*T] - return vs - - -def make_matrix(A, T): - """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method - to convert it to T*T array.""" - if not isinstance(A, np.ndarray): - return A.matrix(T) - else: - return A diff --git a/sequence_jacobian/models/__init__.py b/sequence_jacobian/models/__init__.py deleted file mode 100644 index 29259f6..0000000 --- a/sequence_jacobian/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Specific Model Implementations""" \ No newline at end of file diff --git a/sequence_jacobian/models/hank.py b/sequence_jacobian/models/hank.py deleted file mode 100644 index 8c2ce79..0000000 --- a/sequence_jacobian/models/hank.py +++ /dev/null @@ -1,223 +0,0 @@ -import numpy as np -from numba import vectorize, njit - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het - - -'''Part 1: HA block''' - - -def household_init(a_grid, e_grid, r, w, eis, T): - fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - return fininc, Va - - -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): - """Single backward iteration step using endogenous gridpoint method for households with separable CRRA utility.""" - # this one is useful to do internally - ws = w * e_grid - - # uc(z_t, a_t) - uc_nextgrid = (beta * Pi_p) @ Va_p - - # c(z_t, a_t) and n(z_t, a_t) - c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) - - # c(z_t, a_{t-1}) and n(z_t, a_{t-1}) - lhs = c_nextgrid - ws[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] - rhs = (1 + r) * a_grid - c = utils.interpolate.interpolate_y(lhs, rhs, c_nextgrid) - n = utils.interpolate.interpolate_y(lhs, rhs, n_nextgrid) - - # test constraints, replace if needed - a = rhs + ws[:, np.newaxis] * n + T[:, np.newaxis] - c - iconst = np.nonzero(a < a_grid[0]) - a[iconst] = a_grid[0] - - # if there exist states/prior asset levels such that households want to borrow, compute the constrained - # solution for consumption and labor supply - if iconst[0].size != 0 and iconst[1].size != 0: - c[iconst], n[iconst] = solve_cn(ws[iconst[0]], rhs[iconst[1]] + T[iconst[0]] - a_grid[0], - eis, frisch, vphi, Va_p[iconst]) - - # calculate marginal utility to go backward - Va = (1 + r) * c ** (-1 / eis) - - # efficiency units of labor which is what really matters - n_e = e_grid[:, np.newaxis] * n - - return Va, a, c, n, n_e - - -def transfers(pi_e, Div, Tax, e_grid): - # default incidence rules are proportional to skill - tax_rule, div_rule = e_grid, e_grid # scale does not matter, will be normalized anyway - - div = Div / np.sum(pi_e * div_rule) * div_rule - tax = Tax / np.sum(pi_e * tax_rule) * tax_rule - T = div - tax - return T - - -household.add_hetinput(transfers, verbose=False) - - -@njit -def cn(uc, w, eis, frisch, vphi): - """Return optimal c, n as function of u'(c) given parameters""" - return uc ** (-eis), (w * uc / vphi) ** frisch - - -def solve_cn(w, T, eis, frisch, vphi, uc_seed): - uc = solve_uc(w, T, eis, frisch, vphi, uc_seed) - return cn(uc, w, eis, frisch, vphi) - - -@vectorize -def solve_uc(w, T, eis, frisch, vphi, uc_seed): - """Solve for optimal uc given in log uc space. - - max_{c, n} c**(1-1/eis) + vphi*n**(1+1/frisch) s.t. c = w*n + T - """ - log_uc = np.log(uc_seed) - for i in range(30): - ne, ne_p = netexp(log_uc, w, T, eis, frisch, vphi) - if abs(ne) < 1E-11: - break - else: - log_uc -= ne / ne_p - else: - raise ValueError("Cannot solve constrained household's problem: No convergence after 30 iterations!") - - return np.exp(log_uc) - - -@njit -def netexp(log_uc, w, T, eis, frisch, vphi): - """Return net expenditure as a function of log uc and its derivative.""" - c, n = cn(np.exp(log_uc), w, eis, frisch, vphi) - ne = c - w * n - T - - # c and n have elasticities of -eis and frisch wrt log u'(c) - c_loguc = -eis * c - n_loguc = frisch * n - netexp_loguc = c_loguc - w * n_loguc - - return ne, netexp_loguc - - -'''Part 2: Simple blocks and hetinput''' - - -@simple -def firm(Y, w, Z, pi, mu, kappa): - L = Y / Z - Div = Y - w * L - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y - return L, Div - - -@simple -def monetary(pi, rstar, phi): - r = (1 + rstar(-1) + phi * pi(-1)) / (1 + pi) - 1 - return r - - -@simple -def fiscal(r, B): - Tax = r * B - return Tax - - -@simple -def mkt_clearing(A, N_e, C, L, Y, B, pi, mu, kappa): - asset_mkt = A - B - labor_mkt = N_e - L - goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y - return asset_mkt, labor_mkt, goods_mkt - - -@simple -def nkpc(pi, w, Z, Y, r, mu, kappa): - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ - - (1 + pi).apply(np.log) - return nkpc_res - - -@simple -def income_state_vars(rho_s, sigma_s, nS): - e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) - return e_grid, pi_e, Pi - - -@simple -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@simple -def partial_steady_state_solution(B_Y, Y, mu, r, kappa, Z, pi): - B = B_Y - w = 1 / mu - Div = (1 - w) - Tax = r * B - - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) - - return B, w, Div, Tax, nkpc_res - - -'''Part 3: Steady state''' - - -def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1.2, B_Y=5.6, rho_s=0.966, sigma_s=0.5, - kappa=0.1, phi=1.5, nS=7, amax=150, nA=500): - """Solve steady state of full GE model. Calibrate (beta, vphi) to hit target for interest rate and Y.""" - - # set up grid - a_grid = utils.discretize.agrid(amax=amax, n=nA) - e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) - - # solve analytically what we can - B = B_Y - w = 1 / mu - Div = (1 - w) - Tax = r * B - T = transfers(pi_e, Div, Tax, e_grid) - - # initialize guess for policy function iteration - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - - # residual function - def res(x): - beta_loc, vphi_loc = x - # precompute constrained c and n which don't depend on Va - if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001: - raise ValueError('Clearly invalid inputs') - out = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta_loc, - eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc) - return np.array([out['A'] - B, out['N_e'] - 1]) - - # solve for beta, vphi - (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) - - # extra evaluation for reporting - ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis, - Div=Div, Tax=Tax, frisch=frisch, vphi=vphi) - - # check Walras's law - goods_mkt = 1 - ss['C'] - assert np.abs(goods_mkt) < 1E-8 - - # add aggregate variables - ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, - 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, - 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) - - return ss diff --git a/sequence_jacobian/models/krusell_smith.py b/sequence_jacobian/models/krusell_smith.py deleted file mode 100644 index 82e4559..0000000 --- a/sequence_jacobian/models/krusell_smith.py +++ /dev/null @@ -1,125 +0,0 @@ -import numpy as np -import scipy.optimize as opt - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het - - -'''Part 1: HA block''' - - -def household_init(a_grid, e_grid, r, w, eis): - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - return Va - - -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, e_grid, r, w, beta, eis): - """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. - - Parameters - ---------- - Va_p : array (S*A), marginal value of assets tomorrow - Pi_p : array (S*S), Markov matrix for skills tomorrow - a_grid : array (A), asset grid - e_grid : array (A), skill grid - r : scalar, ex-post real interest rate - w : scalar, wage - beta : scalar, discount rate today - eis : scalar, elasticity of intertemporal substitution - - Returns - ---------- - Va : array (S*A), marginal value of assets today - a : array (S*A), asset policy today - c : array (S*A), consumption policy today - """ - uc_nextgrid = (beta * Pi_p) @ Va_p - c_nextgrid = uc_nextgrid ** (-eis) - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) - utils.optimized_routines.setmin(a, a_grid[0]) - c = coh - a - Va = (1 + r) * c ** (-1 / eis) - return Va, a, c - - -'''Part 2: Simple Blocks''' - - -@simple -def firm(K, L, Z, alpha, delta): - r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta - w = (1 - alpha) * Z * (K(-1) / L) ** alpha - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return r, w, Y - - -@simple -def mkt_clearing(K, A, Y, C, delta): - asset_mkt = A - K - goods_mkt = Y - C - delta * K - return asset_mkt, goods_mkt - - -@simple -def income_state_vars(rho, sigma, nS): - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - return e_grid, Pi - - -@simple -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@simple -def firm_steady_state_solution(r, delta, alpha): - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - - return Z, K, Y, w - - -'''Part 3: Steady state''' - - -def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, sigma=0.5, - nS=7, nA=500, amax=200): - """Solve steady state of full GE model. Calibrate beta to hit target for interest rate.""" - # set up grid - a_grid = utils.discretize.agrid(amax=amax, n=nA) - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - - # solve for aggregates analytically - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - - # figure out initializer - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - - # solve for beta consistent with this - beta_min = lb / (1 + r) - beta_max = ub / (1 + r) - beta, sol = opt.brentq(lambda bet: household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=bet, eis=eis, - Va=Va)['A'] - K, beta_min, beta_max, full_output=True) - if not sol.converged: - raise ValueError('Steady-state solver did not converge.') - - # extra evaluation to report variables - ss = household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=beta, eis=eis, Va=Va) - ss.update({'Pi': Pi, 'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, - 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, - 'rho': rho, 'nS': nS, 'asset_mkt': ss["A"] - K}) - - return ss diff --git a/sequence_jacobian/models/rbc.py b/sequence_jacobian/models/rbc.py deleted file mode 100644 index 61db8b0..0000000 --- a/sequence_jacobian/models/rbc.py +++ /dev/null @@ -1,95 +0,0 @@ -import numpy as np - -from ..blocks.simple_block import simple - -'''Part 1: Simple blocks''' - - -@simple -def firm(K, L, Z, alpha, delta): - r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta - w = (1 - alpha) * Z * (K(-1) / L) ** alpha - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return r, w, Y - - -@simple -def household(K, L, w, eis, frisch, vphi, delta): - C = (w / vphi / L ** (1 / frisch)) ** eis - I = K - (1 - delta) * K(-1) - return C, I - - -@simple -def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): - goods_mkt = Y - C - I - euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - walras = C + K - (1 + r) * K(-1) - w * L - return goods_mkt, euler, walras - - -@simple -def steady_state_solution(Y, L, r, eis, delta, alpha, frisch): - # 1. Solve for beta to hit r - beta = 1 / (1 + r) - - # 2. Solve for K to hit goods_mkt - K = alpha * Y / (r + delta) - w = (1 - alpha) * Y / L - C = w * L + (1 + r) * K(-1) - K - I = delta * K - goods_mkt = Y - C - I - - # 3. Solve for Z to hit Y - Z = Y * K ** (-alpha) * L ** (alpha - 1) - - # 4. Solve for vphi to hit L - vphi = w * C ** (-1 / eis) * L ** (-1 / frisch) - - # 5. Have to return euler because it's a target - euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - - return beta, K, w, C, I, goods_mkt, Z, vphi, euler - - -'''Part 2: Steady state''' - - -def rbc_ss(r=0.01, eis=1, frisch=1, delta=0.025, alpha=0.11): - """Solve steady state of simple RBC model. - - Parameters - ---------- - r : scalar, real interest rate - eis : scalar, elasticity of intertemporal substitution (1/sigma) - frisch : scalar, Frisch elasticity (1/nu) - delta : scalar, depreciation rate - alpha : scalar, capital share - - Returns - ------- - ss : dict, steady state values - """ - # solve for aggregates analytically - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * K ** alpha - I = delta * K - C = Y - I - - # preference params - beta = 1 / (1 + r) - vphi = w * C ** (-1 / eis) - - # check Walras's law, goods market clearing, and the euler equation - walras = C - r * K - w - goods_mkt = Y - C - I - euler = C ** (-1 / eis) - beta * (1 + r) * C ** (-1 / eis) - assert np.abs(walras) < 1E-12 - - return {'beta': beta, 'eis': eis, 'frisch': frisch, 'vphi': vphi, 'delta': delta, 'alpha': alpha, - 'Z': Z, 'K': K, 'I': I, 'Y': Y, 'L': 1, 'C': C, 'w': w, 'r': r, 'walras': walras, 'euler': euler, - 'goods_mkt': goods_mkt} - diff --git a/sequence_jacobian/models/two_asset.py b/sequence_jacobian/models/two_asset.py deleted file mode 100644 index 403bdbe..0000000 --- a/sequence_jacobian/models/two_asset.py +++ /dev/null @@ -1,423 +0,0 @@ -# pylint: disable=E1120 -import numpy as np -from numba import guvectorize - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het, hetoutput -from ..blocks.solved_block import solved -from ..blocks.support.simple_displacement import apply_function - -'''Part 1: HA block''' - - -def household_init(b_grid, a_grid, e_grid, eis, tax, w): - z_grid = income(e_grid, tax, w, 1) - Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - - return z_grid, Va, Vb - - -@het(exogenous='Pi', policy=['b', 'a'], backward=['Vb', 'Va'], backward_init=household_init) # order as in grid! -def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): - # require that k is decreasing (new) - assert k_grid[1] < k_grid[0], 'kappas in k_grid must be decreasing!' - - # precompute Psi1(a', a) on grid of (a', a) for steps 3 and 5 - Psi1 = get_Psi_and_deriv(a_grid[:, np.newaxis], - a_grid[np.newaxis, :], ra, chi0, chi1, chi2)[1] - - # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === - # (take discounted expectation of tomorrow's value function) - Wb = matrix_times_first_dim(beta * Pi_p, Vb_p) - Wa = matrix_times_first_dim(beta * Pi_p, Va_p) - W_ratio = Wa / Wb - - # === STEP 3: a'(z, b', a) for UNCONSTRAINED === - - # for each (z, b', a), linearly interpolate to find a' between gridpoints - # satisfying optimality condition W_ratio == 1+Psi1 - i, pi = lhs_equals_rhs_interpolate(W_ratio, 1 + Psi1) - - # use same interpolation to get Wb and then c - a_endo_unc = utils.interpolate.apply_coord(i, pi, a_grid) - c_endo_unc = utils.interpolate.apply_coord(i, pi, Wb) ** (-eis) - - # === STEP 4: b'(z, b, a), a'(z, b, a) for UNCONSTRAINED === - - # solve out budget constraint to get b(z, b', a) - b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1 + ra) * a_grid) - + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) - - # interpolate this b' -> b mapping to get b -> b', so we have b'(z, b, a) - # and also use interpolation to get a'(z, b, a) - # (note utils.interpolate.interpolate_coord and utils.interpolate.apply_coord work on last axis, - # so we need to swap 'b' to the last axis, then back when done) - i, pi = utils.interpolate.interpolate_coord(b_endo.swapaxes(1, 2), b_grid) - a_unc = utils.interpolate.apply_coord(i, pi, a_endo_unc.swapaxes(1, 2)).swapaxes(1, 2) - b_unc = utils.interpolate.apply_coord(i, pi, b_grid).swapaxes(1, 2) - - # === STEP 5: a'(z, kappa, a) for CONSTRAINED === - - # for each (z, kappa, a), linearly interpolate to find a' between gridpoints - # satisfying optimality condition W_ratio/(1+kappa) == 1+Psi1, assuming b'=0 - lhs_con = W_ratio[:, 0:1, :] / (1 + k_grid[np.newaxis, :, np.newaxis]) - i, pi = lhs_equals_rhs_interpolate(lhs_con, 1 + Psi1) - - # use same interpolation to get Wb and then c - a_endo_con = utils.interpolate.apply_coord(i, pi, a_grid) - c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis]) ** (-eis) - * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) - - # === STEP 6: a'(z, b, a) for CONSTRAINED === - - # solve out budget constraint to get b(z, kappa, a), enforcing b'=0 - b_endo = (c_endo_con + a_endo_con - + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1 + ra) * a_grid) - + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) - - # interpolate this kappa -> b mapping to get b -> kappa - # then use the interpolated kappa to get a', so we have a'(z, b, a) - # (utils.interpolate.interpolate_y does this in one swoop, but since it works on last - # axis, we need to swap kappa to last axis, and then b back to middle when done) - a_con = utils.interpolate.interpolate_y(b_endo.swapaxes(1, 2), b_grid, - a_endo_con.swapaxes(1, 2)).swapaxes(1, 2) - - # === STEP 7: obtain policy functions and update derivatives of value function === - - # combine unconstrained solution and constrained solution, choosing latter - # when unconstrained goes below minimum b - a, b = a_unc.copy(), b_unc.copy() - b[b <= b_grid[0]] = b_grid[0] - a[b <= b_grid[0]] = a_con[b <= b_grid[0]] - - # calculate adjustment cost and its derivative - Psi, _, Psi2 = get_Psi_and_deriv(a, a_grid, ra, chi0, chi1, chi2) - - # solve out budget constraint to get consumption and marginal utility - c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b - uc = c ** (-1 / eis) - - # for GE wage Phillips curve we'll need endowment-weighted utility too - u = e_grid[:, np.newaxis, np.newaxis] * uc - - # update derivatives of value function using envelope conditions - Va = (1 + ra - Psi2) * uc - Vb = (1 + rb) * uc - - return Va, Vb, a, b, c, u - - -def income(e_grid, tax, w, N): - z_grid = (1 - tax) * w * N * e_grid - return z_grid - - -# A potential hetoutput to include with the above HetBlock -@hetoutput() -def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): - chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) - return chi - - -household.add_hetinput(income, verbose=False) -household.add_hetoutput(adjustment_costs, verbose=False) - - -"""Supporting functions for HA block""" - - -def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): - """Adjustment cost Psi(ap, a) and its derivatives with respect to - first argument (ap) and second argument (a)""" - a_with_return = (1 + ra) * a - a_change = ap - a_with_return - abs_a_change = np.abs(a_change) - sign_change = np.sign(a_change) - - adj_denominator = a_with_return + chi0 - core_factor = (abs_a_change / adj_denominator) ** (chi2 - 1) - - Psi = chi1 / chi2 * abs_a_change * core_factor - Psi1 = chi1 * sign_change * core_factor - Psi2 = -(1 + ra) * (Psi1 + (chi2 - 1) * Psi / adj_denominator) - return Psi, Psi1, Psi2 - - -def matrix_times_first_dim(A, X): - """Take matrix A times vector X[:, i1, i2, i3, ... , in] separately - for each i1, i2, i3, ..., in. Same output as A @ X if X is 1D or 2D""" - # flatten all dimensions of X except first, then multiply, then restore shape - return (A @ X.reshape(X.shape[0], -1)).reshape(X.shape) - - -def addouter(z, b, a): - """Take outer sum of three arguments: result[i, j, k] = z[i] + b[j] + a[k]""" - return z[:, np.newaxis, np.newaxis] + b[:, np.newaxis] + a - - -@guvectorize(['void(float64[:], float64[:,:], uint32[:], float64[:])'], '(ni),(ni,nj)->(nj),(nj)') -def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): - """ - Given lhs (i) and rhs (i,j), for each j, find the i such that - - lhs[i] > rhs[i,j] and lhs[i+1] < rhs[i+1,j] - - i.e. where given j, lhs == rhs in between i and i+1. - - Also return the pi such that - - pi*(lhs[i] - rhs[i,j]) + (1-pi)*(lhs[i+1] - rhs[i+1,j]) == 0 - - i.e. such that the point at pi*i + (1-pi)*(i+1) satisfies lhs == rhs by linear interpolation. - - If lhs[0] < rhs[0,j] already, just return u=0 and pi=1. - - ***IMPORTANT: Assumes that solution i is monotonically increasing in j - and that lhs - rhs is monotonically decreasing in i.*** - """ - - ni, nj = rhs.shape - assert len(lhs) == ni - - i = 0 - for j in range(nj): - while True: - if lhs[i] < rhs[i, j]: - break - elif i < nj - 1: - i += 1 - else: - break - - if i == 0: - iout[j] = 0 - piout[j] = 1 - else: - iout[j] = i - 1 - err_upper = rhs[i, j] - lhs[i] - err_lower = rhs[i - 1, j] - lhs[i - 1] - piout[j] = err_upper / (err_upper - err_lower) - - -'''Part 2: Simple blocks''' - - -@simple -def pricing(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) \ - / (1 + r(+1)) - (1 + pi).apply(np.log) - return nkpc - - -@simple -def arbitrage(div, p, r): - equity = div(+1) + p(+1) - p * (1 + r(+1)) - return equity - - -@simple -def labor(Y, w, K, Z, alpha): - N = (Y / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) - mc = w * N / (1 - alpha) / Y - return N, mc - - -@simple -def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): - inv = (K / K(-1) - 1) / (delta * epsI) + 1 - Q - val = alpha * Z(+1) * (N(+1) / K) ** (1 - alpha) * mc(+1) - (K(+1) / K - (1 - delta) + (K(+1) / K - 1) ** 2 / ( - 2 * delta * epsI)) + K(+1) / K * Q(+1) - (1 + r(+1)) * Q - return inv, val - - -@simple -def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): - psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y - k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) - I = K - (1 - delta) * K(-1) + k_adjust - div = Y - w * N - I - psip - return psip, I, div - - -@simple -def taylor(rstar, pi, phi): - i = rstar + phi * pi - return i - - -@simple -def fiscal(r, w, N, G, Bg): - tax = (r * Bg + G) / w / N - return tax - - -@simple -def finance(i, p, pi, r, div, omega, pshare): - rb = r - omega - ra = pshare(-1) * (div + p) / p(-1) + (1 - pshare(-1)) * (1 + r) - 1 - fisher = 1 + i(-1) - (1 + r) * (1 + pi) - return rb, ra, fisher - - -@simple -def wage(pi, w): - piw = (1 + pi) * w / w(-1) - 1 - return piw - - -@simple -def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ - (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) - return wnkpc - - -@simple -def mkt_clearing(p, A, B, Bg, C, I, G, Chi, psip, omega, Y): - wealth = A + B - asset_mkt = p + Bg - wealth - goods_mkt = C + I + G + Chi + psip + omega * B - Y - return asset_mkt, wealth, goods_mkt - - -@simple -def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): - b_grid = utils.discretize.agrid(amax=bmax, n=nB) - a_grid = utils.discretize.agrid(amax=amax, n=nA) - k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) - - return b_grid, a_grid, k_grid, e_grid, Pi - - -@simple -def share_value(p, tot_wealth, Bh): - pshare = p / (tot_wealth - Bh) - return pshare - - -@simple -def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): - """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" - # 1. Solve for markup to hit total wealth - p = tot_wealth - Bg - mc = 1 - r * (p - K) / Y - mup = 1 / mc - wealth = tot_wealth - - # 2. Solve for capital share to hit K - alpha = (r + delta) * K / Y / mc - - # 3. Solve for TFP to hit N (Y cannot be used, because it is an unknown of the DAG) - Z = Y * K ** (-alpha) * N ** (alpha - 1) - - # 4. Solve for w such that piw = 0 - w = mc * (1 - alpha) * Y / N - piw = 0 - - return p, mc, mup, wealth, alpha, Z, w, piw - - -@simple -def partial_ss_step2(tax, w, U, N, muw, frisch): - """Solves for (vphi) to hit (wnkpc).""" - vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) - wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw - return vphi, wnkpc - - -'''Part 3: Steady state''' - - -def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, - muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi2=2, epsI=4, omega=0.005, kappaw=0.1, - phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, - verbose=True): - """Solve steady state of full GE model. Calibrate (beta, vphi, chi1, alpha, mup, Z) to hit targets for - (r, tot_wealth, Bh, K, Y=N=1). - """ - - # set up grid - b_grid = utils.discretize.agrid(amax=bmax, n=nB) - a_grid = utils.discretize.agrid(amax=amax, n=nA) - k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) - - # solve analytically what we can - I = delta * K - mc = 1 - r * (tot_wealth - Bg - K) - alpha = (r + delta) * K / mc - mup = 1 / mc - Z = K ** (-alpha) - w = (1 - alpha) * mc - tax = (r * Bg + G) / w - div = 1 - w - I - p = div / r - ra = r - rb = r - omega - - # figure out initializer - z_grid = income(e_grid, tax, w, 1) - Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - - # residual function - def res(x): - beta_loc, chi1_loc = x - if beta_loc > 0.999 / (1 + r) or chi1_loc < 0.5: - raise ValueError('Clearly invalid inputs') - out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, - k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) - asset_mkt = out['A'] + out['B'] - p - Bg - return np.array([asset_mkt, out['B'] - Bh]) - - # solve for beta, vphi, omega - (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), verbose=verbose) - - # extra evaluation to report variables - ss = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, - k_grid=k_grid, beta=beta, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1, chi2=chi2) - - # other things of interest - vphi = (1 - tax) * w * ss['U'] / muw - pshare = p / (tot_wealth - Bh) - - # calculate aggregate adjustment cost and check Walras's law - chi = get_Psi_and_deriv(ss.internal["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] - Chi = np.vdot(ss.internal["household"]['D'], chi) - goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 - - ss.internal["household"].update({"chi": chi}) - ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, - 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, - 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, - 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, - 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, - 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, 'goods_mkt': goods_mkt, - 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], - 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1 / mup), - 'wnkpc': kappaw * (vphi * ss["N"] ** (1 + 1 / frisch) - (1 - tax) * w * ss["N"] * ss["U"] / muw), - 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1 - alpha) * mc - delta - r}) - return ss - - -'''Part 4: Solved blocks for transition dynamics/Jacobian calculation''' - - -@solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver="brentq") -def pricing_solved(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ - (1 + r(+1)) - (1 + pi).apply(np.log) - return nkpc - - -@solved(unknowns={'p': (5, 15)}, targets=['equity'], solver="brentq") -def arbitrage_solved(div, p, r): - equity = div(+1) + p(+1) - p * (1 + r(+1)) - return equity - - -production_solved = solved(block_list=[labor, investment], unknowns={'Q': 1., 'K': 10.}, - targets=['inv', 'val'], solver="broyden_custom") diff --git a/sequence_jacobian/nonlinear.py b/sequence_jacobian/nonlinear.py deleted file mode 100644 index b45ef9e..0000000 --- a/sequence_jacobian/nonlinear.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Functions for solving for the non-linear transition dynamics provided a given shock path (e.g. solving MIT shocks)""" - -import numpy as np - -from .utilities import misc, graph -from .jacobian.drivers import get_H_U -from .jacobian.support import pack_vectors, unpack_vectors - - -def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=False, - returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): - """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. - - Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. - - Parameters - ---------- - block_list : list, blocks in model (SimpleBlocks or HetBlocks) - ss : dict, all steady-state information - exogenous : dict, all shocked Z go here, must all have same length T - unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) - targets : list, targets of SHADE DAG, the 'H' in H(U, Z) - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k - (allows more efficient interpolation) - returnindividual: [optional] bool, flag to return individual outcomes from HetBlock.td - tol : [optional] scalar, for convergence of Newton's method we require |H| SteadyStateDict: - raise NotImplementedError(f'{type(self)} does not implement .steady_state()') - - def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: - raise NotImplementedError(f'{type(self)} does not implement .impulse_nonlinear()') - - def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: - raise NotImplementedError(f'{type(self)} does not implement .impulse_linear()') - - def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = None, - T: int = None, **kwargs) -> JacobianDict: - raise NotImplementedError(f'{type(self)} does not implement .jacobian()') - - def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], - unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], - targets: Union[Array, Dict[str, Union[str, Real]]], - solver: Optional[str] = "", **kwargs) -> SteadyStateDict: - """Evaluate a general equilibrium steady state of Block given a `calibration` - and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and - the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - solver = solver if solver else provide_solver_default(unknowns) - return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) - - def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], - unknowns: List[str], targets: List[str], - Js: Optional[Dict[str, JacobianDict]] = None, - **kwargs) -> ImpulseDict: - """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - irf_nonlin_gen_eq = td_solve(blocks, ss, - exogenous={k: v for k, v in exogenous.items()}, - unknowns=unknowns, targets=targets, Js=Js, **kwargs) - return ImpulseDict(irf_nonlin_gen_eq) - - def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], - unknowns: List[str], targets: List[str], - T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = None, - **kwargs) -> ImpulseDict: - """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) - return ImpulseDict(irf_lin_gen_eq) - - def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], - exogenous: List[str], - unknowns: List[str], targets: List[str], - T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = None, - **kwargs) -> JacobianDict: - """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks - at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) diff --git a/sequence_jacobian/steady_state/__init__.py b/sequence_jacobian/steady_state/__init__.py deleted file mode 100644 index 017e34b..0000000 --- a/sequence_jacobian/steady_state/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Steady state computation and support functions""" diff --git a/sequence_jacobian/steady_state/classes.py b/sequence_jacobian/steady_state/classes.py deleted file mode 100644 index cb8238f..0000000 --- a/sequence_jacobian/steady_state/classes.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Various classes to support the computation of steady states""" - -from copy import deepcopy - -from ..utilities.misc import dict_diff - - -class SteadyStateDict: - def __init__(self, data, internal=None): - self.toplevel = {} - self.internal = {} - self.update(data, internal_namespaces=internal) - - def __repr__(self): - if self.internal: - return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" - else: - return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" - - def __iter__(self): - return iter(self.toplevel) - - def __getitem__(self, k): - if isinstance(k, str): - return self.toplevel[k] - else: - try: - return {ki: self.toplevel[ki] for ki in k} - except TypeError: - raise TypeError(f'Key {k} needs to be a string or an iterable (list, set, etc) of strings') - - def __setitem__(self, k, v): - self.toplevel[k] = v - - def keys(self): - return self.toplevel.keys() - - def values(self): - return self.toplevel.values() - - def items(self): - return self.toplevel.items() - - def update(self, data, internal_namespaces=None): - if isinstance(data, SteadyStateDict): - self.internal.update(deepcopy(data.internal)) - self.toplevel.update(deepcopy(data.toplevel)) - else: - toplevel = deepcopy(data) - if internal_namespaces is not None: - # Construct the internal namespace from the Block object, if a Block is provided - if hasattr(internal_namespaces, "internal"): - internal_namespaces = {internal_namespaces.name: {k: v for k, v in deepcopy(data).items() if k in - internal_namespaces.internal}} - - # Remove the internal data from `data` if it's there - for internal_dict in internal_namespaces.values(): - toplevel = dict_diff(toplevel, internal_dict) - - self.toplevel.update(toplevel) - self.internal.update(internal_namespaces) - else: - self.toplevel.update(toplevel) - - def difference(self, data_to_remove): - return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) diff --git a/sequence_jacobian/steady_state/drivers.py b/sequence_jacobian/steady_state/drivers.py deleted file mode 100644 index dbe6e9c..0000000 --- a/sequence_jacobian/steady_state/drivers.py +++ /dev/null @@ -1,306 +0,0 @@ -"""A general function for computing a model's steady state variables and parameters values""" - -import numpy as np -import scipy.optimize as opt -from copy import deepcopy -from functools import partial - -from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ - extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ - subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs -from .classes import SteadyStateDict -from ..utilities import solvers, graph, misc - - -# Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, dissolve=None, - sort_blocks=True, helper_blocks=None, helper_targets=None, - consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, - block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, - constrained_method="linear_continuation", constrained_kwargs=None): - """ - For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. - - blocks: `list` - A list of blocks, which include the types: SimpleBlock, HetBlock, SolvedBlock, CombinedBlock - calibration: `dict` - The pre-specified values of variables/parameters provided to the steady state computation - unknowns: `dict` - A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver - targets: `dict` - A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG - dissolve: `list` - A list of blocks, either SolvedBlock or CombinedBlock, where block-level unknowns are removed and subsumed - by the top-level unknowns, effectively removing the "solve" components of the blocks - sort_blocks: `bool` - Whether the blocks need to be topologically sorted (only False when this function is called from within a - Block object, like CombinedBlock, that has already pre-sorted the blocks) - helper_blocks: `list` - A list of blocks that replace some of the equations in the DAG to aid steady state calculation - helper_targets: `list` - A list of target names that are handled by the helper blocks - consistency_check: `bool` - If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values - without the assistance of helper blocks and see if the targets are still hit - ttol: `float` - The tolerance for the targets---how close the user wants the computed target values to equal the desired values - ctol: `float` - The tolerance for the consistency check---how close the user wants the computed target values, without the - use of helper blocks, to equal the desired values - fragile: `bool` - Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails - block_kwargs: `dict` - A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any - potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that - Block sub-class. - verbose: `bool` - Display the content of optional print statements within the solver for more responsive feedback - solver: `string` - The name of the numerical solver that the user would like to user. Can either be a custom solver the user - implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root - solver_kwargs: `dict` - The keyword arguments that the user's chosen solver requires to run - constrained_method: `str` - When using solvers that typically only take an initial value, x0, we provide a few options for manipulating - the solver to account for bounds when finding a solution. These methods are described in the - constrained_multivariate_residual function - constrained_kwargs: - The keyword arguments that the user's chosen constrained method requires to run - - return: ss_values: `dict` - A dictionary containing all of the pre-specified values and computed values from the steady state computation - """ - - dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ - instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, - block_kwargs, solver_kwargs, constrained_kwargs) - - # Initial setup of blocks, targets, and dictionary of steady state values to be returned - blocks_all = blocks + helper_blocks - targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - - helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - helper_targets = {t: targets[t] for t in targets if t in helper_targets} - helper_outputs = {} - - ss_values = SteadyStateDict(calibration) - ss_values.update(helper_targets) - - if sort_blocks: - topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) - else: - topsorted = range(len(blocks + helper_blocks)) - - def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, consistency_check=False): - ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) - - # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper - # block subsetting - # Progress through the DAG computing the resulting steady state values based on the unknown_values - # provided to the residual function - for i in topsorted: - if not include_helpers and blocks_all[i] in helper_blocks: - continue - outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, - dissolve=dissolve, verbose=verbose, **block_kwargs) - if include_helpers and blocks_all[i] in helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) - ss_values.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) - - # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the - # dict of ss_values to compute the "unknown_solutions" - return compute_target_values(targets_dict, ss_values) - - if helper_blocks: - unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, - helper_targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=ttol, verbose=verbose, fragile=fragile) - else: - unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=ttol, verbose=verbose, fragile=fragile) - - # Check that the solution is consistent with what would come out of the DAG without the helper blocks - if consistency_check and helper_blocks: - # Add the unknowns not handled by helpers into the DAG to be checked. - unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) - - cresid = abs(np.max(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), - include_helpers=False, consistency_check=True))) - run_consistency_check(cresid, ctol=ctol, fragile=fragile) - - # Update to set the solutions for the steady state values of the unknowns - ss_values.update(unknowns_solved) - - return ss_values - - -def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): - """Evaluate the .ss method of a block, given a dictionary of potential arguments""" - if toplevel_unknowns is None: - toplevel_unknowns = {} - block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False - - # Add the block's internal variables as inputs, if the block has an internal attribute - input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - - # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them - # at the provided set of unknowns if included in dissolve. - valid_input_kwargs = misc.input_kwarg_list(block.steady_state) - input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if block in dissolve and "solver" in valid_input_kwargs: - input_kwarg_dict["solver"] = "solved" - input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} - elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: - raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," - f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," - f" {set(toplevel_unknowns)}.\n" - f"If the user provides a set of top-level unknowns that subsume block-level unknowns," - f" it must be explicitly declared in `dissolve`.") - - return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) - - -def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, - constrained_method="linear_continuation", constrained_kwargs=None, - tol=2e-12, verbose=False, fragile=False): - """ - Given a residual function (constructed within steady_state) and a set of bounds or initial values for - the set of unknowns, solve for the root. - TODO: Implemented as a hidden method as of now because this function relies on the structure of steady_state - specifically and will not work with a generic residual function, due to the way it currently expects residual - to call variables not provided as arguments explicitly but that exist in its enclosing scope. - - residual: `function` - A function to be supplied to a numerical solver that takes unknown values as arguments - and returns computed targets. - unknowns: `dict` - Refer to the `steady_state` function docstring for the "unknowns" variable - targets: `dict` - Refer to the `steady_state` function docstring for the "targets" variable - tol: `float` - The absolute convergence tolerance of the computed target to the desired target value in the numerical solver - solver: `str` - Refer to the `steady_state` function docstring for the "solver" variable - solver_kwargs: - Refer to the `steady_state` function docstring for the "solver_kwargs" variable - - return: The root[s] of the residual function as either a scalar (float) or a list of floats - """ - if residual_kwargs is None: - residual_kwargs = {} - - scipy_optimize_uni_solvers = ["bisect", "brentq", "brenth", "ridder", "toms748", "newton", "secant", "halley"] - scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", - "excitingmixing", "krylov", "df-sane"] - - # Construct a reduced residual function, which contains addl context of unknowns, targets, and keyword arguments. - # This is to bypass issues with passing a residual function that requires contextual, positional arguments - # separate from the unknown values that need to be solved for into the multivariate solvers - residual_f = partial(residual, targets, unknowns.keys(), **residual_kwargs) - - if solver is None: - raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") - elif solver in scipy_optimize_uni_solvers: - initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) - result = opt.root_scalar(residual_f, method=solver, xtol=tol, - **initial_values_or_bounds, **solver_kwargs) - if not result.converged: - raise ValueError(f"Steady-state solver, {solver}, did not converge.") - unknown_solutions = result.root - elif solver in scipy_optimize_multi_solvers: - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns, fragile=fragile) - # If no bounds were provided - if not bounds: - result = opt.root(residual_f, initial_values, - method=solver, tol=tol, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - result = opt.root(constrained_residual, initial_values, - method=solver, tol=tol, **solver_kwargs) - if not result.success: - raise ValueError(f"Steady-state solver, {solver}, did not converge." - f" The termination status is {result.status}.") - unknown_solutions = list(result.x) - # TODO: Implement a more general interface for custom solvers, so we don't need to add new elifs at this level - # everytime a new custom solver is implemented. - elif solver == "broyden_custom": - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) - # If no bounds were provided - if not bounds: - unknown_solutions, _ = solvers.broyden_solver(residual_f, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, - verbose=verbose, tol=tol, **solver_kwargs) - unknown_solutions = list(unknown_solutions) - elif solver == "newton_custom": - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) - # If no bounds were provided - if not bounds: - unknown_solutions, _ = solvers.newton_solver(residual_f, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - unknown_solutions = list(unknown_solutions) - elif solver == "solved": - # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution - # simply call residual_f once to populate the `ss_values` dict - residual_f(unknowns.values()) - unknown_solutions = unknowns.values() - else: - raise RuntimeError(f"steady_state is not yet compatible with {solver}.") - - return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) - - -def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, - solver, solver_kwargs, constrained_method="linear_continuation", - constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): - """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets - with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" - # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, - # to populate the `ss_values` dict with the unknown values that: - # a) are handled by helper blocks and b) are excludable from the main DAG - # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed - unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] - targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) - - # Subset out the unknowns and targets that are not excludable from the main DAG loop - unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) - targets_non_excl = misc.dict_diff(targets, helper_targets) - - # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of - # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and - # we can omit them and their corresponding `unknowns` in the main DAG. - if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): - unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, - solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=tol, verbose=verbose, fragile=fragile) - # If targets handled by helpers and excludable from the main DAG are not satisfied then - # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, - # and they remain a part of the main DAG when solving. - else: - unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=tol, verbose=verbose, fragile=fragile) - return unknown_solutions diff --git a/sequence_jacobian/steady_state/support.py b/sequence_jacobian/steady_state/support.py deleted file mode 100644 index 88374b6..0000000 --- a/sequence_jacobian/steady_state/support.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Various lower-level functions to support the computation of steady states""" - -import warnings -from numbers import Real -import numpy as np - - -def instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, - block_kwargs, solver_kwargs, constrained_kwargs): - """Instantiate mutable types from `None` default values in the steady_state function""" - if dissolve is None: - dissolve = [] - if helper_blocks is None and helper_targets is None: - helper_blocks = [] - helper_targets = [] - elif helper_blocks is not None and helper_targets is None: - raise ValueError("If the user has provided `helper_blocks`, the kwarg `helper_targets` must be specified" - " indicating which target variables are handled by the `helper_blocks`.") - elif helper_blocks is None and helper_targets is not None: - raise ValueError("If the user has provided `helper_targets`, the kwarg `helper_blocks` must be specified" - " indicating which helper blocks handle the `helper_targets`") - if block_kwargs is None: - block_kwargs = {} - if solver_kwargs is None: - solver_kwargs = {} - if constrained_kwargs is None: - constrained_kwargs = {} - - return dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs - - -def provide_solver_default(unknowns): - if len(unknowns) == 1: - bounds = list(unknowns.values())[0] - if not isinstance(bounds, tuple) or bounds[0] > bounds[1]: - raise ValueError("Unable to find a compatible one-dimensional solver with provided `unknowns`.\n" - " Please provide valid lower/upper bounds, e.g. unknowns = {`a`: (0, 1)}") - else: - return "brentq" - elif len(unknowns) > 1: - init_values = list(unknowns.values()) - if not np.all([isinstance(v, Real) for v in init_values]): - raise ValueError("Unable to find a compatible multi-dimensional solver with provided `unknowns`.\n" - " Please provide valid initial values, e.g. unknowns = {`a`: 1, `b`: 2}") - else: - return "broyden_custom" - else: - raise ValueError("`unknowns` is empty! Please provide a dict of keys/values equal to the number of unknowns" - " that need to be solved for.") - - -def run_consistency_check(cresid, ctol=1e-9, fragile=False): - if cresid > ctol: - if fragile: - raise RuntimeError(f"The target values evaluated for the proposed set of unknowns produce a " - f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" - f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" - f" If this is not an issue, adjust ctol accordingly.") - else: - warnings.warn(f"The target values evaluated for the proposed set of unknowns produce a " - f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" - f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" - f" If this is not an issue, adjust ctol accordingly.") - - -# Allow targets to be specified in the following formats -# 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) -# 2) target = {"r": 0.01} (allowing for the target to be non-zero) -# 3) target = {"K": "A"} (allowing the target to be another variable in potential_args) -def compute_target_values(targets, potential_args): - """ - For a given set of target specifications and potential arguments available, compute the targets. - Called as the return value for the residual function when utilizing the numerical solver. - - targets: Refer to `steady_state` function docstring - potential_args: Refer to the `steady_state` function docstring for the "calibration" variable - - return: A `float` (if computing a univariate target) or an `np.ndarray` (if using a multivariate target) - """ - target_values = np.empty(len(targets)) - for (i, t) in enumerate(targets): - v = targets[t] if isinstance(targets, dict) else 0 - if type(v) == str: - target_values[i] = potential_args[t] - potential_args[v] - else: - target_values[i] = potential_args[t] - v - # TODO: Implement feature to allow for an arbitrary explicit function expression as a potential target value - # e.g. targets = {"goods_mkt": "Y - C - I"}, so long as the expression is only comprise of generic numerical - # operators and variables solved for along the DAG prior to reaching the target. - - # Univariate solvers require float return values (and not lists) - if len(targets) == 1: - return target_values[0] - else: - return target_values - - -def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): - """Find the set of unknowns that the `helper_blocks` solve for""" - unknowns_handled_by_helpers = {} - for block in helper_blocks: - unknowns_handled_by_helpers.update({u: unknowns_all[u] for u in block.outputs if u in unknowns_all}) - - n_unknowns = len(unknowns_handled_by_helpers) - n_targets = len(helper_targets) - if n_unknowns != n_targets: - raise ValueError(f"The provided helper_blocks handle {n_unknowns} unknowns != {n_targets} targets." - f" User must specify an equal number of unknowns/targets solved for by helper blocks.") - - return unknowns_handled_by_helpers - - -def find_excludable_helper_blocks(blocks_all, helper_indices, helper_unknowns, helper_targets): - """Of the set of helper_unknowns and helper_targets, find the ones that can be excluded from the main DAG - for the purposes of numerically solving unknowns.""" - excludable_helper_unknowns = {} - excludable_helper_targets = {} - for i in helper_indices: - excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) - excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) - return excludable_helper_unknowns, excludable_helper_targets - - -def extract_univariate_initial_values_or_bounds(unknowns): - val = next(iter(unknowns.values())) - if np.isscalar(val): - return {"x0": val} - else: - return {"bracket": (val[0], val[1])} - - -def extract_multivariate_initial_values_and_bounds(unknowns, fragile=False): - """Provided a dict mapping names of unknowns to initial values/bounds, return separate dicts of - the initial values and bounds. - Note: For one-sided bounds, simply put np.inf/-np.inf as the other side of the bounds, so there is - no ambiguity about which is the unconstrained side. -""" - initial_values = [] - multi_bounds = {} - for k, v in unknowns.items(): - if np.isscalar(v): - initial_values.append(v) - elif len(v) == 2: - if fragile: - raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." - f" the values of `unknowns` must either be a scalar, pertaining to a" - f" single initial value for the root solver to begin from," - f" a length 2 tuple, pertaining to a lower bound and an upper bound," - f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") - else: - warnings.warn("Interpreting values of `unknowns` from length 2 tuple as lower and upper bounds" - " and averaging them to get a scalar initial value to provide to the solver.") - initial_values.append((v[0] + v[1])/2) - elif len(v) == 3: - lb, iv, ub = v - assert lb < iv < ub - initial_values.append(iv) - multi_bounds[k] = (lb, ub) - else: - raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." - f" the values of `unknowns` must either be a scalar, pertaining to a" - f" single initial value for the root solver to begin from," - f" a length 2 tuple, pertaining to a lower bound and an upper bound," - f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") - - return np.asarray(initial_values), multi_bounds - - -def residual_with_linear_continuation(residual, bounds, eval_at_boundary=False, - boundary_epsilon=1e-4, penalty_scale=1e1, - verbose=False): - """Modify a residual function to implement bounds by an additive penalty for exceeding the boundaries - provided, scaled by the amount the guess exceeds the boundary. - - e.g. For residual function f(x), desiring x in (0, 1) (so assuming eval_at_boundary = False) - If the guess for x is 1.1 then we will censor to x_censored = 1 - boundary_epsilon, and return - f(x_censored) + penalty (where the penalty does not require re-evaluating f() which may be costly) - - residual: `function` - The function whose roots we want to solve for - bounds: `dict` - A dict mapping the names of the unknowns (`str`) to length two tuples corresponding to the lower and upper - bounds. - eval_at_boundary: `bool` - Whether to allow the residual function to be evaluated at exactly the boundary values or not. - Think of it as whether the solver will treat the bounds as creating a closed or open set for the search space. - boundary_epsilon: `float` - The amount to adjust the proposed guess, x, by to calculate the censored value of the residual function, - when the proposed guess exceeds the boundaries. - penalty_scale: `float` - The linear scaling factor for adjusting the penalty for the proposed unknown values exceeding the boundary. - verbose: `bool` - Whether to print out additional information for how the constrained residual function is behaving during - optimization. Useful for tuning the solver. - """ - lbs = np.asarray([v[0] for v in bounds.values()]) - ubs = np.asarray([v[1] for v in bounds.values()]) - - def constr_residual(x, residual_cache=[]): - """Implements a constrained residual function, where any attempts to evaluate x outside of the - bounds provided will result in a linear penalty function scaled by `penalty_scale`. - - Note: We are purposefully using residual_cache as a mutable default argument to cache the most recent - valid evaluation (maintain state between function calls) of the residual function to induce solvers - to backstep if they encounter a region of the search space that returns nan values. - See Hitchhiker's Guide to Python post on Mutable Default Arguments: "When the Gotcha Isn't a Gotcha" - """ - if eval_at_boundary: - x_censored = np.where(x < lbs, lbs, x) - x_censored = np.where(x > ubs, ubs, x_censored) - else: - x_censored = np.where(x < lbs, lbs + boundary_epsilon, x) - x_censored = np.where(x > ubs, ubs - boundary_epsilon, x_censored) - - residual_censored = residual(x_censored) - - if verbose: - print(f"Attempted x is {x}") - print(f"Censored x is {x_censored}") - print(f"The residual_censored is {residual_censored}") - - if np.any(np.isnan(residual_censored)): - # Provide a scaled penalty to the solver when trying to evaluate residual() in an undefined region - residual_censored = residual_cache[0] * penalty_scale - - if verbose: - print(f"The new residual_censored is {residual_censored}") - else: - if not residual_cache: - residual_cache.append(residual_censored) - else: - residual_cache[0] = residual_censored - - if verbose: - print(f"The residual_cache is {residual_cache[0]}") - - # Provide an additive, scaled penalty to the solver when trying to evaluate residual() outside of the boundary - residual_with_boundary_penalty = residual_censored + \ - (x - x_censored) * penalty_scale * residual_censored - return residual_with_boundary_penalty - - return constr_residual - - -def constrained_multivariate_residual(residual, bounds, method="linear_continuation", verbose=False, - **constrained_kwargs): - """Return a constrained version of the residual function, which accounts for bounds, using the specified method. - See the docstring of the specific method of interest for further details.""" - if method == "linear_continuation": - return residual_with_linear_continuation(residual, bounds, verbose=verbose, **constrained_kwargs) - # TODO: Implement logistic transform as another option for constrained multivariate residual - else: - raise ValueError(f"Method {method} for constrained multivariate root-finding has not yet been implemented.") diff --git a/sequence_jacobian/utilities/__init__.py b/sequence_jacobian/utilities/__init__.py deleted file mode 100644 index e8d1bab..0000000 --- a/sequence_jacobian/utilities/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Utilities relating to: interpolation, forward step/transition, grids and Markov chains, solvers, sorting, etc.""" - -from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers diff --git a/sequence_jacobian/utilities/differentiate.py b/sequence_jacobian/utilities/differentiate.py deleted file mode 100644 index 758c67e..0000000 --- a/sequence_jacobian/utilities/differentiate.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Numerical differentiation""" - -from .misc import make_tuple - - -def numerical_diff(func, ssinputs_dict, shock_dict, h=1E-4, y_ss_list=None): - """Differentiate function numerically via forward difference, i.e. calculate - - f'(xss)*shock = (f(xss + h*shock) - f(xss))/h - - for small h. (Variable names inspired by application of differentiating around ss.) - - Parameters - ---------- - func : function, 'f' to be differentiated - ssinputs_dict : dict, values in 'xss' around which to differentiate - shock_dict : dict, values in 'shock' for which we're taking derivative - (keys in shock_dict are weak subset of keys in ssinputs_dict) - h : [optional] scalar, scaling of forward difference 'h' - y_ss_list : [optional] list, value of y=f(xss) if we already have it - - Returns - ---------- - dy_list : list, output f'(xss)*shock of numerical differentiation - """ - # compute ss output if not supplied - if y_ss_list is None: - y_ss_list = make_tuple(func(**ssinputs_dict)) - - # response to small shock - shocked_inputs = {**ssinputs_dict, **{k: ssinputs_dict[k] + h * shock for k, shock in shock_dict.items()}} - y_list = make_tuple(func(**shocked_inputs)) - - # scale responses back up, dividing by h - dy_list = [(y - y_ss) / h for y, y_ss in zip(y_list, y_ss_list)] - - return dy_list - - -def numerical_diff_symmetric(func, ssinputs_dict, shock_dict, h=1E-4): - """Same as numerical_diff, but differentiate numerically using central (symmetric) difference, i.e. - - f'(xss)*shock = (f(xss + h*shock) - f(xss - h*shock))/(2*h) - """ - - # response to small shock in each direction - shocked_inputs_up = {**ssinputs_dict, **{k: ssinputs_dict[k] + h * shock for k, shock in shock_dict.items()}} - y_up_list = make_tuple(func(**shocked_inputs_up)) - - shocked_inputs_down = {**ssinputs_dict, **{k: ssinputs_dict[k] - h * shock for k, shock in shock_dict.items()}} - y_down_list = make_tuple(func(**shocked_inputs_down)) - - # scale responses back up, dividing by h - dy_list = [(y_up - y_down) / (2*h) for y_up, y_down in zip(y_up_list, y_down_list)] - - return dy_list diff --git a/sequence_jacobian/utilities/discretize.py b/sequence_jacobian/utilities/discretize.py deleted file mode 100644 index 95ace8f..0000000 --- a/sequence_jacobian/utilities/discretize.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Grids and Markov chains""" - -import numpy as np -from scipy.stats import norm - - -def agrid(amax, n, amin=0): - """Create grid between amin-pivot and amax+pivot that is equidistant in logs.""" - pivot = np.abs(amin) + 0.25 - a_grid = np.geomspace(amin + pivot, amax + pivot, n) - pivot - a_grid[0] = amin # make sure *exactly* equal to amin - return a_grid - - -# TODO: Temporarily include the old way of constructing grids from ikc_old for comparability of results -def agrid_old(amax, N, amin=0, frac=1/25): - """crappy discretization method we've been using, generates N point - log-spaced grid between bmin and bmax, choosing pivot such that 'frac' of - total log space between log(1+amin) and log(1+amax) beneath it""" - apivot = (1+amin)**(1-frac)*(1+amax)**frac - 1 - a = np.geomspace(amin+apivot,amax+apivot,N) - apivot - a[0] = amin - return a - - -def stationary(Pi, pi_seed=None, tol=1E-11, maxit=10_000): - """Find invariant distribution of a Markov chain by iteration.""" - if pi_seed is None: - pi = np.ones(Pi.shape[0]) / Pi.shape[0] - else: - pi = pi_seed - - for it in range(maxit): - pi_new = pi @ Pi - if np.max(np.abs(pi_new - pi)) < tol: - break - pi = pi_new - else: - raise ValueError(f'No convergence after {maxit} forward iterations!') - pi = pi_new - - return pi - - -def mean(x, pi): - """Mean of discretized random variable with support x and probability mass function pi.""" - return np.sum(pi * x) - - -def variance(x, pi): - """Variance of discretized random variable with support x and probability mass function pi.""" - return np.sum(pi * (x - np.sum(pi * x)) ** 2) - - -def std(x, pi): - """Standard deviation of discretized random variable with support x and probability mass function pi.""" - return np.sqrt(variance(x, pi)) - - -def cov(x, y, pi): - """Covariance of two discretized random variables with supports x and y common probability mass function pi.""" - return np.sum(pi * (x - mean(x, pi)) * (y - mean(y, pi))) - - -def corr(x, y, pi): - """Correlation of two discretized random variables with supports x and y common probability mass function pi.""" - return cov(x, y, pi) / (std(x, pi) * std(y, pi)) - - -def markov_tauchen(rho, sigma, N=7, m=3, normalize=True): - """Tauchen method discretizing AR(1) s_t = rho*s_(t-1) + eps_t. - - Parameters - ---------- - rho : scalar, persistence - sigma : scalar, unconditional sd of s_t - N : int, number of states in discretized Markov process - m : scalar, discretized s goes from approx -m*sigma to m*sigma - - Returns - ---------- - y : array (N), states proportional to exp(s) s.t. E[y] = 1 - pi : array (N), stationary distribution of discretized process - Pi : array (N*N), Markov matrix for discretized process - """ - - # make normalized grid, start with cross-sectional sd of 1 - s = np.linspace(-m, m, N) - ds = s[1] - s[0] - sd_innov = np.sqrt(1 - rho ** 2) - - # standard Tauchen method to generate Pi given N and m - Pi = np.empty((N, N)) - Pi[:, 0] = norm.cdf(s[0] - rho * s + ds / 2, scale=sd_innov) - Pi[:, -1] = 1 - norm.cdf(s[-1] - rho * s - ds / 2, scale=sd_innov) - for j in range(1, N - 1): - Pi[:, j] = (norm.cdf(s[j] - rho * s + ds / 2, scale=sd_innov) - - norm.cdf(s[j] - rho * s - ds / 2, scale=sd_innov)) - - # invariant distribution and scaling - pi = stationary(Pi) - s *= (sigma / np.sqrt(variance(s, pi))) - if normalize: - y = np.exp(s) / np.sum(pi * np.exp(s)) - else: - y = s - - return y, pi, Pi - - -def markov_rouwenhorst(rho, sigma, N=7): - """Rouwenhorst method analog to markov_tauchen""" - - # Explicitly typecast N as an integer, since when the grid constructor functions - # (e.g. the function that makes a_grid) are implemented as blocks, they interpret the integer-valued calibration - # as a float. - N = int(N) - - # parametrize Rouwenhorst for n=2 - p = (1 + rho) / 2 - Pi = np.array([[p, 1 - p], [1 - p, p]]) - - # implement recursion to build from n=3 to n=N - for n in range(3, N + 1): - P1, P2, P3, P4 = (np.zeros((n, n)) for _ in range(4)) - P1[:-1, :-1] = p * Pi - P2[:-1, 1:] = (1 - p) * Pi - P3[1:, :-1] = (1 - p) * Pi - P4[1:, 1:] = p * Pi - Pi = P1 + P2 + P3 + P4 - Pi[1:-1] /= 2 - - # invariant distribution and scaling - pi = stationary(Pi) - s = np.linspace(-1, 1, N) - s *= (sigma / np.sqrt(variance(s, pi))) - y = np.exp(s) / np.sum(pi * np.exp(s)) - - return y, pi, Pi diff --git a/sequence_jacobian/utilities/forward_step.py b/sequence_jacobian/utilities/forward_step.py deleted file mode 100644 index 96ffa3b..0000000 --- a/sequence_jacobian/utilities/forward_step.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Forward iteration of distribution on grid and related functions. - - - forward_step_1d - - forward_step_2d - - apply law of motion for distribution to go from D_{t-1} to D_t - - - forward_step_shock_1d - - forward_step_shock_2d - - forward_step linearized, used in part 1 of fake news algorithm to get curlyDs - - - forward_step_transpose_1d - - forward_step_transpose_2d - - transpose of forward_step, used in part 2 of fake news algorithm to get curlyPs -""" - -import numpy as np -from numba import njit - - -@njit -def forward_step_1d(D, Pi_T, x_i, x_pi): - """Single forward step to update distribution using exogenous Markov transition Pi and - policy x_i and x_pi for one-dimensional endogenous state. - - Efficient implementation of D_t = Lam_{t-1}' @ D_{t-1} using sparsity of the endogenous - part of Lam_{t-1}'. - - Note that it takes Pi_T, the transpose of Pi, as input rather than transposing itself; - this is so that when it is applied repeatedly, we can precalculate a transpose stored in - correct order rather than a view. - - Parameters - ---------- - D : array (S*X), beginning-of-period distribution over s_t, x_(t-1) - Pi_T : array (S*S), transpose Markov matrix that maps s_t to s_(t+1) - x_i : int array (S*X), left gridpoint of endogenous policy - x_pi : array (S*X), weight on left gridpoint of endogenous policy - - Returns - ---------- - Dnew : array (S*X), beginning-of-next-period dist s_(t+1), x_t - """ - - # first update using endogenous policy - nZ, nX = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - i = x_i[iz, ix] - pi = x_pi[iz, ix] - d = D[iz, ix] - Dnew[iz, i] += d * pi - Dnew[iz, i+1] += d * (1 - pi) - - # then using exogenous transition matrix - return Pi_T @ Dnew - - -def forward_step_2d(D, Pi_T, x_i, y_i, x_pi, y_pi): - """Like forward_step_1d but with two-dimensional endogenous state, policies given by x and y""" - Dmid = forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi) - nZ, nX, nY = Dmid.shape - return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) - - -@njit -def forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi): - """Endogenous update part of forward_step_2d""" - nZ, nX, nY = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i[iz, ix, iy] - iyp = y_i[iz, ix, iy] - beta = x_pi[iz, ix, iy] - alpha = y_pi[iz, ix, iy] - - Dnew[iz, ixp, iyp] += alpha * beta * D[iz, ix, iy] - Dnew[iz, ixp+1, iyp] += alpha * (1 - beta) * D[iz, ix, iy] - Dnew[iz, ixp, iyp+1] += (1 - alpha) * beta * D[iz, ix, iy] - Dnew[iz, ixp+1, iyp+1] += (1 - alpha) * (1 - beta) * D[iz, ix, iy] - return Dnew - - -@njit -def forward_step_shock_1d(Dss, Pi_T, x_i_ss, x_pi_shock): - """forward_step_1d linearized wrt x_pi""" - # first find effect of shock to endogenous policy - nZ, nX = Dss.shape - Dshock = np.zeros_like(Dss) - for iz in range(nZ): - for ix in range(nX): - i = x_i_ss[iz, ix] - dshock = x_pi_shock[iz, ix] * Dss[iz, ix] - Dshock[iz, i] += dshock - Dshock[iz, i + 1] -= dshock - - # then apply exogenous transition matrix to update - return Pi_T @ Dshock - - -def forward_step_shock_2d(Dss, Pi_T, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): - """forward_step_2d linearized wrt x_pi and y_pi""" - Dmid = forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock) - nZ, nX, nY = Dmid.shape - return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) - - -@njit -def forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): - """Endogenous update part of forward_step_shock_2d""" - nZ, nX, nY = Dss.shape - Dshock = np.zeros_like(Dss) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i_ss[iz, ix, iy] - iyp = y_i_ss[iz, ix, iy] - alpha = x_pi_ss[iz, ix, iy] - beta = y_pi_ss[iz, ix, iy] - - dalpha = x_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] - dbeta = y_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] - - Dshock[iz, ixp, iyp] += dalpha * beta + alpha * dbeta - Dshock[iz, ixp+1, iyp] += dbeta * (1-alpha) - beta * dalpha - Dshock[iz, ixp, iyp+1] += dalpha * (1-beta) - alpha * dbeta - Dshock[iz, ixp+1, iyp+1] -= dalpha * (1-beta) + dbeta * (1-alpha) - return Dshock - - -@njit -def forward_step_transpose_1d(D, Pi, x_i, x_pi): - """Transpose of forward_step_1d""" - # first update using exogenous transition matrix - D = Pi @ D - - # then update using (transpose) endogenous policy - nZ, nX = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - i = x_i[iz, ix] - pi = x_pi[iz, ix] - Dnew[iz, ix] = pi * D[iz, i] + (1-pi) * D[iz, i+1] - return Dnew - - -def forward_step_transpose_2d(D, Pi, x_i, y_i, x_pi, y_pi): - """Transpose of forward_step_2d.""" - nZ, nX, nY = D.shape - Dmid = (Pi @ D.reshape(nZ, -1)).reshape(nZ, nX, nY) - return forward_step_transpose_endo_2d(Dmid, x_i, y_i, x_pi, y_pi) - - -@njit -def forward_step_transpose_endo_2d(D, x_i, y_i, x_pi, y_pi): - """Endogenous update part of forward_step_transpose_2d""" - nZ, nX, nY = D.shape - Dnew = np.empty_like(D) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i[iz, ix, iy] - iyp = y_i[iz, ix, iy] - alpha = x_pi[iz, ix, iy] - beta = y_pi[iz, ix, iy] - - Dnew[iz, ix, iy] = (alpha * beta * D[iz, ixp, iyp] + alpha * (1-beta) * D[iz, ixp, iyp+1] + - (1-alpha) * beta * D[iz, ixp+1, iyp] + - (1-alpha) * (1-beta) * D[iz, ixp+1, iyp+1]) - return Dnew - - diff --git a/sequence_jacobian/utilities/graph.py b/sequence_jacobian/utilities/graph.py deleted file mode 100644 index aa01ecd..0000000 --- a/sequence_jacobian/utilities/graph.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Topological sort and related code""" - - -def block_sort(blocks, helper_blocks=None, calibration=None, return_io=False): - """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. - - Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's - inferred) that indicate their aggregate inputs and outputs - - Importantly, because including helper blocks in a blocks without additional measures - can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the - steady_state computation to resolve these cycles. - e.g. Consider Krusell Smith: - Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the helper block - because the "firm" block outputs "r", which the helper block takes as an input. - However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes - "K" as an input. - This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of helper block and the cycle would be resolved. - - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - ignore_helpers: `bool` - A boolean indicating whether to account for/return the indices of helper blocks contained in blocks - Set to true when sorting for td and jac calculations - helper_indices: `list` - A list of indices corresponding to the helper blocks in the blocks - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using helper blocks. Read above docstring for more detail - return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `blocks` - """ - # TODO: Decide whether we want to break out the input and output argument tracking and return to - # a different function... currently it's very convenient to slot it into block_sort directly, but it - # does clutter up the function body - if return_io: - # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(blocks, helper_blocks=helper_blocks, - return_output_args=True) - - # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, - helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(dep), inargs, outargs - else: - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(blocks, helper_blocks=helper_blocks) - - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(blocks, outmap, calibration=calibration, helper_blocks=helper_blocks) - - return topological_sort(dep) - - -def topological_sort(dep, names=None): - """Given directed graph pointing from each node to the nodes it depends on, topologically sort nodes""" - - # get complete set version of dep, and its reversal, and build initial stack of nodes with no dependencies - dep, revdep = complete_reverse_graph(dep) - nodeps = [n for n in dep if not dep[n]] - topsorted = [] - - # Kahn's algorithm: find something with no dependency, delete its edges and update - while nodeps: - n = nodeps.pop() - topsorted.append(n) - for n2 in revdep[n]: - dep[n2].remove(n) - if not dep[n2]: - nodeps.append(n2) - - # should be done: topsorted should be topologically sorted with same # of elements as original graphs! - if len(topsorted) != len(dep): - cycle_ints = find_cycle(dep, dep.keys() - set(topsorted)) - assert cycle_ints is not None, 'topological sort failed but no cycle, THIS SHOULD NEVER EVER HAPPEN' - cycle = [names[i] for i in cycle_ints] if names else cycle_ints - raise Exception(f'Topological sort failed: cyclic dependency {" -> ".join([str(n) for n in cycle])}') - - return topsorted - - -def construct_output_map(blocks, helper_blocks=None, return_output_args=False): - """Construct a map of outputs to the indices of the blocks that produce them. - - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - helper_blocks: `list` - A list of helper blocks, designed to aid steady state computation, to include in the sort - return_output_args: `bool` - A boolean indicating whether to track and return the full set of output arguments of all of the blocks - in `blocks` - """ - if helper_blocks is None: - helper_blocks = [] - - outmap = dict() - outargs = set() - for num, block in enumerate(blocks + helper_blocks): - # Find the relevant set of outputs corresponding to a block - if hasattr(block, "outputs"): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: - # Because some of the outputs of a helper block are, by construction, outputs that also appear in the - # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering - # throwing this ValueError - if o in outmap and block not in helper_blocks: - raise ValueError(f'{o} is output twice') - - # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share - # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap: - outmap[o] = num - if return_output_args: - outargs.add(o) - else: - continue - if return_output_args: - return outmap, outargs - else: - return outmap - - -def construct_dependency_graph(blocks, outmap, helper_blocks=None, - calibration=None, return_input_args=False): - """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where - this set is the set of blocks that the key block is dependent on. - - outmap is the output map (output to block index mapping) created by construct_output_map. - - See the docstring of block_sort for more details about the other arguments. - """ - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - - dep = {num: set() for num in range(len(blocks + helper_blocks))} - inargs = set() - for num, block in enumerate(blocks + helper_blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - if return_input_args: - inargs.add(i) - # Each potential input to a given block will either be 1) output by another block, - # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into - # the steady-state computation via the `calibration' dict. - # If the block is a helper block, then we want to check the calibration to see if the potential - # input is a pre-specified variable/parameter, and if it is then we will not add the block that - # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic - # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in helper_blocks): - dep[num].add(outmap[i]) - if return_input_args: - return dep, inargs - else: - return dep - - -def find_outputs_that_are_intermediate_inputs(blocks, helper_blocks=None): - """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. - This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. - - See the docstring of construct_output_map for more details about the arguments. - """ - if helper_blocks is None: - helper_blocks = [] - - required = set() - outmap = construct_output_map(blocks, helper_blocks=helper_blocks) - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - if i in outmap: - required.add(i) - return required - - -def complete_reverse_graph(gph): - """Given directed graph represented as a dict from nodes to iterables of nodes, return representation of graph that - is complete (i.e. has each vertex pointing to some iterable, even if empty), and a complete version of reversed too. - Have returns be sets, for easy removal""" - - revgph = {n: set() for n in gph} - for n, e in gph.items(): - for n2 in e: - n2_edges = revgph.setdefault(n2, set()) - n2_edges.add(n) - - gph_missing_n = revgph.keys() - gph.keys() - gph = {**{k: set(v) for k, v in gph.items()}, **{n: set() for n in gph_missing_n}} - return gph, revgph - - -def find_cycle(dep, onlyset=None): - """Return list giving cycle if there is one, otherwise None""" - - # supposed to look only within 'onlyset', so filter out everything else - if onlyset is not None: - dep = {k: (set(v) & set(onlyset)) for k, v in dep.items() if k in onlyset} - - tovisit = set(dep.keys()) - stack = SetStack() - while tovisit or stack: - if stack: - # if stack has something, still need to proceed with DFS - n = stack.top() - if dep[n]: - # if there are any dependencies left, let's look at them - n2 = dep[n].pop() - if n2 in stack: - # we have a cycle, since this is already in our stack - i2loc = stack.index(n2) - return stack[i2loc:] + [stack[i2loc]] - else: - # no cycle, visit this node only if we haven't already visited it - if n2 in tovisit: - tovisit.remove(n2) - stack.add(n2) - else: - # if no dependencies left, then we're done with this node, so let's forget about it - stack.pop(n) - else: - # nothing left on stack, let's start the DFS from something new - n = tovisit.pop() - stack.add(n) - - # if we never find a cycle, we're done - return None - - -class SetStack: - """Stack implemented with list but tests membership with set to be efficient in big cases""" - - def __init__(self): - self.myset = set() - self.mylist = [] - - def add(self, x): - self.myset.add(x) - self.mylist.append(x) - - def pop(self): - x = self.mylist.pop() - self.myset.remove(x) - return x - - def top(self): - return self.mylist[-1] - - def index(self, x): - return self.mylist.index(x) - - def __contains__(self, x): - return x in self.myset - - def __len__(self): - return len(self.mylist) - - def __getitem__(self, i): - return self.mylist.__getitem__(i) - - def __repr__(self): - return self.mylist.__repr__() - - diff --git a/sequence_jacobian/utilities/interpolate.py b/sequence_jacobian/utilities/interpolate.py deleted file mode 100644 index 1a0ac5f..0000000 --- a/sequence_jacobian/utilities/interpolate.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Efficient linear interpolation exploiting monotonicity. - - Interpolates increasing query points xq against increasing data points x. - - - interpolate_y: (x, xq, y) -> yq - get interpolated values of yq at xq - - - interpolate_coord: (x, xq) -> (xqi, xqpi) - get representation xqi, xqpi of xq interpolated against x - xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] - - - apply_coord: (xqi, xqpi, y) -> yq - use representation xqi, xqpi to get yq at xq - yq = xqpi * y[xqi] + (1-xqpi) * y[xqi+1] - - Composing interpolate_coord and apply_coord gives interpolate_y. - - All three functions are written for vectors but can be broadcast to other dimensions - since we use Numba's guvectorize decorator. In these cases, interpolation is always - done on the final dimension. -""" - -import numpy as np -from numba import njit, guvectorize - - -@guvectorize(['void(float64[:], float64[:], float64[:], float64[:])'], '(n),(nq),(n)->(nq)') -def interpolate_y(x, xq, y, yq): - """Efficient linear interpolation exploiting monotonicity. - - Complexity O(n+nq), so most efficient when x and xq have comparable number of points. - Extrapolates linearly when xq out of domain of x. - - Parameters - ---------- - x : array (n), ascending data points - xq : array (nq), ascending query points - y : array (n), data points - - Returns - ---------- - yq : array (nq), interpolated points - """ - nxq, nx = xq.shape[0], x.shape[0] - - xi = 0 - x_low = x[0] - x_high = x[1] - for xqi_cur in range(nxq): - xq_cur = xq[xqi_cur] - while xi < nx - 2: - if x_high >= xq_cur: - break - xi += 1 - x_low = x_high - x_high = x[xi + 1] - - xqpi_cur = (x_high - xq_cur) / (x_high - x_low) - yq[xqi_cur] = xqpi_cur * y[xi] + (1 - xqpi_cur) * y[xi + 1] - - -@guvectorize(['void(float64[:], float64[:], uint32[:], float64[:])'], '(n),(nq)->(nq),(nq)') -def interpolate_coord(x, xq, xqi, xqpi): - """Get representation xqi, xqpi of xq interpolated against x: - xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] - - Parameters - ---------- - x : array (n), ascending data points - xq : array (nq), ascending query points - - Returns - ---------- - xqi : array (nq), indices of lower bracketing gridpoints - xqpi : array (nq), weights on lower bracketing gridpoints - """ - nxq, nx = xq.shape[0], x.shape[0] - - xi = 0 - x_low = x[0] - x_high = x[1] - for xqi_cur in range(nxq): - xq_cur = xq[xqi_cur] - while xi < nx - 2: - if x_high >= xq_cur: - break - xi += 1 - x_low = x_high - x_high = x[xi + 1] - - xqpi[xqi_cur] = (x_high - xq_cur) / (x_high - x_low) - xqi[xqi_cur] = xi - - -@guvectorize(['void(int64[:], float64[:], float64[:], float64[:])', - 'void(uint32[:], float64[:], float64[:], float64[:])'], '(nq),(nq),(n)->(nq)') -def apply_coord(x_i, x_pi, y, yq): - """Use representation xqi, xqpi to get yq at xq: - yq = xqpi * y[xqi] + (1-xqpi) * y[xqi+1] - - Parameters - ---------- - xqi : array (nq), indices of lower bracketing gridpoints - xqpi : array (nq), weights on lower bracketing gridpoints - y : array (n), data points - - Returns - ---------- - yq : array (nq), interpolated points - """ - nq = x_i.shape[0] - for iq in range(nq): - y_low = y[x_i[iq]] - y_high = y[x_i[iq]+1] - yq[iq] = x_pi[iq]*y_low + (1-x_pi[iq])*y_high - - -'''Part 2: More robust linear interpolation that does not require monotonicity in query points. - - Intended for general use in interpolating policy rules that we cannot be sure are monotonic. - Only get xqi, xqpi representation, for case where x is one-dimensional, in this application. -''' - - -def interpolate_coord_robust(x, xq, check_increasing=False): - """Linear interpolation exploiting monotonicity only in data x, not in query points xq. - Simple binary search, less efficient but more robust. - xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] - - Main application intended to be universally-valid interpolation of policy rules. - Dimension k is optional. - - Parameters - ---------- - x : array (n), ascending data points - xq : array (k, nq), query points (in any order) - - Returns - ---------- - xqi : array (k, nq), indices of lower bracketing gridpoints - xqpi : array (k, nq), weights on lower bracketing gridpoints - """ - if x.ndim != 1: - raise ValueError('Data input to interpolate_coord_robust must have exactly one dimension') - - if check_increasing and np.any(x[:-1] >= x[1:]): - raise ValueError('Data input to interpolate_coord_robust must be strictly increasing') - - if xq.ndim == 1: - return interpolate_coord_robust_vector(x, xq) - else: - i, pi = interpolate_coord_robust_vector(x, xq.ravel()) - return i.reshape(xq.shape), pi.reshape(xq.shape) - - -@njit -def interpolate_coord_robust_vector(x, xq): - """Does interpolate_coord_robust where xq must be a vector, more general function is wrapper""" - - n = len(x) - nq = len(xq) - xqi = np.empty(nq, dtype=np.uint32) - xqpi = np.empty(nq) - - for iq in range(nq): - if xq[iq] < x[0]: - ilow = 0 - elif xq[iq] > x[-2]: - ilow = n-2 - else: - # start binary search - # should end with ilow and ihigh exactly 1 apart, bracketing variable - ihigh = n-1 - ilow = 0 - while ihigh - ilow > 1: - imid = (ihigh + ilow) // 2 - if xq[iq] > x[imid]: - ilow = imid - else: - ihigh = imid - - xqi[iq] = ilow - xqpi[iq] = (x[ilow+1] - xq[iq]) / (x[ilow+1] - x[ilow]) - - return xqi, xqpi diff --git a/sequence_jacobian/utilities/misc.py b/sequence_jacobian/utilities/misc.py deleted file mode 100644 index cf47e7b..0000000 --- a/sequence_jacobian/utilities/misc.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Assorted other utilities""" - -import numpy as np -import scipy.linalg -import re -import inspect -import warnings -from ..jacobian.classes import JacobianDict - - -def make_tuple(x): - """If not tuple or list, make into tuple with one element. - - Wrapping with this allows user to write, e.g.: - "return r" rather than "return (r,)" - "policy='a'" rather than "policy=('a',)" - """ - return (x,) if not (isinstance(x, tuple) or isinstance(x, list)) else x - - -def input_list(f): - """Return list of function inputs (both positional and keyword arguments)""" - return list(inspect.signature(f).parameters) - - -def input_arg_list(f): - """Return list of function positional arguments *only*""" - arg_list = [] - for p in inspect.signature(f).parameters.values(): - if p.default == p.empty: - arg_list.append(p.name) - return arg_list - - -def input_kwarg_list(f): - """Return list of function keyword arguments *only*""" - kwarg_list = [] - for p in inspect.signature(f).parameters.values(): - if p.default != p.empty: - kwarg_list.append(p.name) - return kwarg_list - - -def output_list(f): - """Scans source code of function to detect statement like - - 'return L, Div' - - and reports the list ['L', 'Div']. - - Important to write functions in this way when they will be scanned by output_list, for - either SimpleBlock or HetBlock. - """ - return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') - - -def numeric_primitive(instance): - # If it is already a primitive, just return it - if type(instance) in {int, float}: - return instance - elif isinstance(instance, np.ndarray): - if np.issubdtype(instance.dtype, np.number): - return np.array(instance) - else: - raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance.dtype}," - f" which is not a valid numeric type.") - elif type(instance) in {tuple, list}: - instance_array = np.asarray(instance) - if np.issubdtype(instance_array.dtype, np.number): - return type(instance)(instance_array) - else: - raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance_array.dtype}," - f" which is not a valid numeric type.") - else: - return instance.real if np.isscalar(instance) else instance.base - - -def demean(x): - return x - x.sum()/x.size - - -# simpler aliases for LU factorization and solution -def factor(X): - return scipy.linalg.lu_factor(X) - - -def factored_solve(Z, y): - return scipy.linalg.lu_solve(Z, y) - - -# The below functions are used in steady_state -def unprime(s): - """Given a variable's name as a `str`, check if the variable is a prime, i.e. has "_p" at the end. - If so, return the unprimed version, if not return itself.""" - if s[-2:] == "_p": - return s[:-2] - else: - return s - - -def uncapitalize(s): - return s[0].lower() + s[1:] - - -def list_diff(l1, l2): - """Returns the list that is the "set difference" between l1 and l2 (based on element values)""" - o_list = [] - for k in set(l1) - set(l2): - o_list.append(k) - return o_list - - -def dict_diff(d1, d2): - """Returns the dictionary that is the "set difference" between d1 and d2 (based on keys, not key-value pairs) - E.g. d1 = {"a": 1, "b": 2}, d2 = {"b": 5}, then dict_diff(d1, d2) = {"a": 1} - """ - o_dict = {} - for k in set(d1.keys()) - set(d2.keys()): - o_dict[k] = d1[k] - return o_dict - - -def smart_set(data): - # We want set to construct a single-element set for strings, i.e. ignoring the .iter method of strings - if isinstance(data, str): - return {data} - else: - return set(data) - - -def smart_zip(keys, values): - """For handling the case where keys and values may be scalars""" - if isinstance(values, float): - return zip(keys, [values]) - else: - return zip(keys, values) - - -def smart_zeros(n): - """Return either the float 0. or a np.ndarray of length 0 depending on whether n > 1""" - if n > 1: - return np.zeros(n) - else: - return 0. - - -def verify_saved_jacobian(block_name, Js, outputs, inputs, T): - """Verify that pre-computed Jacobian has all the right outputs, inputs, and length.""" - if block_name not in Js.keys(): - # don't throw warning, this will happen often for simple blocks - return False - J = Js[block_name] - - if not isinstance(J, JacobianDict): - warnings.warn(f'Js[{block_name}] is not a JacobianDict.') - return False - - if not set(outputs).issubset(set(J.outputs)): - missing = set(outputs).difference(set(J.outputs)) - warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') - return False - - if not set(inputs).issubset(set(J.inputs)): - missing = set(inputs).difference(set(J.inputs)) - warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') - return False - - # Jacobian of simple blocks may have a sparse representation - if T is not None: - Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] - if T != Tsaved: - warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') - return False - - return True diff --git a/sequence_jacobian/utilities/optimized_routines.py b/sequence_jacobian/utilities/optimized_routines.py deleted file mode 100644 index 94f1724..0000000 --- a/sequence_jacobian/utilities/optimized_routines.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Njitted routines to speed up some steps in backward iteration or aggregation""" - -import numpy as np -from numba import njit - - -@njit -def setmin(x, xmin): - """Set 2-dimensional array x where each row is ascending equal to equal to max(x, xmin).""" - ni, nj = x.shape - for i in range(ni): - for j in range(nj): - if x[i, j] < xmin: - x[i, j] = xmin - else: - break - - -@njit -def within_tolerance(x1, x2, tol): - """Efficiently test max(abs(x1-x2)) <= tol for arrays of same dimensions x1, x2.""" - y1 = x1.ravel() - y2 = x2.ravel() - - for i in range(y1.shape[0]): - if np.abs(y1[i] - y2[i]) > tol: - return False - return True - - -@njit -def fast_aggregate(X, Y): - """If X has dims (T, ...) and Y has dims (T, ...), do dot product for each T to get length-T vector. - - Identical to np.sum(X*Y, axis=(1,...,X.ndim-1)) but avoids costly creation of intermediates, useful - for speeding up aggregation in td by factor of 4 to 5.""" - T = X.shape[0] - Xnew = X.reshape(T, -1) - Ynew = Y.reshape(T, -1) - Z = np.empty(T) - for t in range(T): - Z[t] = Xnew[t, :] @ Ynew[t, :] - return Z - - diff --git a/sequence_jacobian/utilities/solvers.py b/sequence_jacobian/utilities/solvers.py deleted file mode 100644 index 076a809..0000000 --- a/sequence_jacobian/utilities/solvers.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Simple nonlinear solvers""" - -import numpy as np -import warnings - - -def newton_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verbose=True): - """Simple line search solver for root x satisfying f(x)=0 using Newton direction. - - Backtracks if input invalid or improvement is not at least half the predicted improvement. - - Parameters - ---------- - f : function, to solve for f(x)=0, input and output are arrays of same length - x0 : array (n), initial guess for x - y0 : [optional] array (n), y0=f(x0), if already known - tol : [optional] scalar, solver exits successfully when |f(x)| < tol - maxcount : [optional] int, maximum number of Newton steps - backtrack_c : [optional] scalar, fraction to backtrack if step unsuccessful, i.e. - if we tried step from x to x+dx, now try x+backtrack_c*dx - - Returns - ---------- - x : array (n), (approximate) root of f(x)=0 - y : array (n), y=f(x), satisfies |y| < tol - """ - - x, y = x0, y0 - if y is None: - y = f(x) - - for count in range(maxcount): - if verbose: - printit(count, x, y) - - if np.max(np.abs(y)) < tol: - return x, y - - J = obtain_J(f, x, y) - dx = np.linalg.solve(J, -y) - - # backtrack at most 29 times - for bcount in range(30): - try: - ynew = f(x + dx) - except ValueError: - if verbose: - print('backtracking\n') - dx *= backtrack_c - else: - predicted_improvement = -np.sum((J @ dx) * y) * ((1 - 1 / 2 ** bcount) + 1) / 2 - actual_improvement = (np.sum(y ** 2) - np.sum(ynew ** 2)) / 2 - if actual_improvement < predicted_improvement / 2: - if verbose: - print('backtracking\n') - dx *= backtrack_c - else: - y = ynew - x += dx - break - else: - raise ValueError('Too many backtracks, maybe bad initial guess?') - else: - raise ValueError(f'No convergence after {maxcount} iterations') - - -def broyden_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verbose=True): - """Similar to newton_solver, but solves f(x)=0 using approximate rather than exact Newton direction, - obtaining approximate Jacobian J=f'(x) from Broyden updating (starting from exact Newton at f'(x0)). - - Backtracks only if error raised by evaluation of f, since improvement criterion no longer guaranteed - to work for any amount of backtracking if Jacobian not exact. - """ - - x, y = x0, y0 - if y is None: - y = f(x) - - for count in range(maxcount): - if verbose: - printit(count, x, y) - - if np.max(np.abs(y)) < tol: - return x, y - - # initialize J with Newton! - if count == 0: - J = obtain_J(f, x, y) - - if len(x) == len(y): - dx = np.linalg.solve(J, -y) - elif len(x) < len(y): - warnings.warn(f"Dimension of x, {len(x)} is less than dimension of y, {len(y)}." - f" Using least-squares criterion to solve for approximate root.") - dx = np.linalg.lstsq(J, -y, rcond=None)[0] - else: - raise ValueError(f"Dimension of x, {len(x)} is greater than dimension of y, {len(y)}." - f" Cannot solve underdetermined system.") - - # backtrack at most 29 times - for bcount in range(30): - # note: can't test for improvement with Broyden because maybe - # the function doesn't improve locally in this direction, since - # J isn't the exact Jacobian - try: - ynew = f(x + dx) - except ValueError: - if verbose: - print('backtracking\n') - dx *= backtrack_c - else: - J = broyden_update(J, dx, ynew - y) - y = ynew - x += dx - break - else: - raise ValueError('Too many backtracks, maybe bad initial guess?') - else: - raise ValueError(f'No convergence after {maxcount} iterations') - - -def obtain_J(f, x, y, h=1E-5): - """Finds Jacobian f'(x) around y=f(x)""" - nx = x.shape[0] - ny = y.shape[0] - J = np.empty((ny, nx)) - - for i in range(nx): - dx = h * (np.arange(nx) == i) - J[:, i] = (f(x + dx) - y) / h - return J - - -def broyden_update(J, dx, dy): - """Returns Broyden update to approximate Jacobian J, given that last change in inputs to function - was dx and led to output change of dy.""" - return J + np.outer(((dy - J @ dx) / np.linalg.norm(dx) ** 2), dx) - - -def printit(it, x, y, **kwargs): - """Convenience printing function for verbose iterations""" - print(f'On iteration {it}') - print(('x = %.3f' + ',%.3f' * (len(x) - 1)) % tuple(x)) - print(('y = %.3f' + ',%.3f' * (len(y) - 1)) % tuple(y)) - for kw, val in kwargs.items(): - print(f'{kw} = {val:.3f}') - print('\n') - diff --git a/sequence_jacobian/visualization/__init__.py b/sequence_jacobian/visualization/__init__.py deleted file mode 100644 index 2850057..0000000 --- a/sequence_jacobian/visualization/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Various tools for plotting and creating visualizations""" diff --git a/sequence_jacobian/visualization/draw_dag.py b/sequence_jacobian/visualization/draw_dag.py deleted file mode 100644 index 9b75ca5..0000000 --- a/sequence_jacobian/visualization/draw_dag.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Provides the functionality for basic DAG visualization""" - -import warnings -from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph - - -# Implement DAG drawing functions as "soft" dependencies to not enforce the installation of graphviz, since -# it's not required for the rest of the sequence-jacobian code to run -try: - """ - DAG Graph routine - Requires installing graphviz package and executables - https://www.graphviz.org/ - - On a mac this can be done as follows: - 1) Download macports at: - https://www.macports.org/install.php - 2) On the command line, install graphviz with macports by typing - sudo port install graphviz - - """ - from graphviz import Digraph - - - def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_helpers=True, calibration=None, - showdag=False, leftright=False, filename='modeldag'): - """ - Visualizes a Directed Acyclic Graph (DAG) of a set of blocks, exogenous variables, unknowns, and targets - - block_list: `list` - Blocks to be represented as nodes within a DAG - exogenous: `list` (optional) - Exogenous variables, to be represented on DAG - unknowns: `list` (optional) - Unknown variables, to be represented on DAG - targets: `list` (optional) - Target variables, to be represented on DAG - ignore_helpers: `bool` - A boolean indicating whether to also draw HelperBlocks contained in block_list into the DAG - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using HelperBlocks. Read `block_sort` docstring for more detail - showdag: `bool` - If True, export and plot pdf file. If false, export png file and do not plot - leftright: `bool` - If True, plots DAG from left to right instead of top to bottom - - return: None - """ - - # To prevent having mutable variables as keyword arguments - exogenous = [] if exogenous is None else exogenous - unknowns = [] if unknowns is None else unknowns - targets = [] if targets is None else targets - - # obtain the topological sort - topsorted = block_sort(block_list, ignore_helpers=ignore_helpers, calibration=calibration) - # get sorted list of blocks - block_list_sorted = [block_list[i] for i in topsorted] - # Obtain the dependency list of the sorted set of blocks - dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted), - ignore_helpers=ignore_helpers, calibration=calibration) - - # Draw DAG - dot = Digraph(comment='Model DAG') - - # Make left-to-right - if leftright: - dot.attr(rankdir='LR', ratio='compress', center='true') - else: - dot.attr(ratio='auto', center='true') - - # add initial nodes (one for exogenous, one for unknowns) provided those are not empty lists - if exogenous: - dot.node('exog', 'exogenous', shape='box') - if unknowns: - dot.node('unknowns', 'unknowns', shape='box') - if targets: - dot.node('targets', 'targets', shape='diamond') - - # add nodes sequentially in order - for i in dep_list_sorted: - if hasattr(block_list_sorted[i], 'hetinput'): - # HA block - dot.node(str(i), 'HA [' + str(i) + ']') - elif hasattr(block_list_sorted[i], 'block_list'): - # Solved block - dot.node(str(i), block_list_sorted[i].block_list[0].f.__name__ + '[solved,' + str(i) + ']') - else: - # Simple block - dot.node(str(i), block_list_sorted[i].f.__name__ + ' [' + str(i) + ']') - - # nodes from exogenous to i (figure out if needed and draw) - if exogenous: - edgelabel = block_list_sorted[i].inputs & set(exogenous) - if len(edgelabel) != 0: - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - dot.edge('exog', str(i), label=str(edgelabel_str)) - - # nodes from unknowns to i (figure out if needed and draw) - if unknowns: - edgelabel = block_list_sorted[i].inputs & set(unknowns) - if len(edgelabel) != 0: - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - dot.edge('unknowns', str(i), label=str(edgelabel_str)) - - # nodes from i to final targets - for target in targets: - if target in block_list_sorted[i].outputs: - dot.edge(str(i), 'targets', label=target) - - # nodes from any interior block to i - for j in dep_list_sorted[i]: - # figure out inputs of i that are also outputs of j - edgelabel = block_list_sorted[i].inputs & block_list_sorted[j].outputs - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - - # draw edge from j to i - dot.edge(str(j), str(i), label=str(edgelabel_str)) - - if showdag: - dot.render('dagexport/' + filename, view=True, cleanup=True) - else: - dot.render('dagexport/' + filename, format='png', cleanup=True) - # print(dot.source) - - - def draw_solved(solvedblock, filename='solveddag'): - # Inspects a solved block by drawing its DAG - draw_dag([solvedblock.block_list[0]], unknowns=solvedblock.unknowns, targets=solvedblock.targets, - filename=filename, showdag=True) - - - def inspect_solved(block_list): - # Inspects all the solved blocks by running through each and drawing its DAG in turn - for block in block_list: - if hasattr(block, 'block_list'): - draw_solved(block, filename=str(block.block_list[0].f.__name__)) -except ImportError: - def draw_dag(*args, **kwargs): - warnings.warn("\nAttempted to use `draw_dag` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass - - - def draw_solved(*args, **kwargs): - warnings.warn("\nAttempted to use `draw_solved` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass - - - def inspect_solved(*args, **kwargs): - warnings.warn("\nAttempted to use `inspect_solved` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass diff --git a/setup.py b/setup.py index c1f64c3..2e69c24 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from pathlib import Path -from setuptools import find_packages, setup +from setuptools import setup, find_packages with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() @@ -12,7 +12,6 @@ setup( name="sequence-jacobian", - packages=find_packages(), python_requires=">=3.7", install_requires=read("requirements.txt").splitlines(), version="0.0.1", @@ -27,4 +26,7 @@ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], + + packages=find_packages(where='src'), + package_dir={'': 'src'}, ) From f5552b065f04647016ffe595f2a6bae56916e4e2 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 16:27:27 -0500 Subject: [PATCH 154/288] Merged manually with Michael's `src` restructuring from today. --- src/sequence_jacobian.egg-info/PKG-INFO | 69 ++ src/sequence_jacobian.egg-info/SOURCES.txt | 7 + .../dependency_links.txt | 1 + src/sequence_jacobian.egg-info/requires.txt | 4 + src/sequence_jacobian.egg-info/top_level.txt | 1 + src/sequence_jacobian/__init__.py | 31 + src/sequence_jacobian/blocks/__init__.py | 1 + .../blocks/auxiliary_blocks/__init__.py | 1 + .../auxiliary_blocks/jacobiandict_block.py | 26 + .../blocks/combined_block.py | 131 +++ src/sequence_jacobian/blocks/het_block.py | 922 ++++++++++++++++++ src/sequence_jacobian/blocks/simple_block.py | 170 ++++ src/sequence_jacobian/blocks/solved_block.py | 126 +++ .../blocks/support/__init__.py | 2 + .../blocks/support/impulse.py | 69 ++ .../blocks/support/simple_displacement.py | 702 +++++++++++++ src/sequence_jacobian/devtools/__init__.py | 3 + src/sequence_jacobian/devtools/analysis.py | 242 +++++ src/sequence_jacobian/devtools/debug.py | 106 ++ src/sequence_jacobian/devtools/deprecate.py | 22 + src/sequence_jacobian/devtools/upgrade.py | 39 + src/sequence_jacobian/estimation.py | 86 ++ src/sequence_jacobian/jacobian/__init__.py | 1 + src/sequence_jacobian/jacobian/classes.py | 471 +++++++++ src/sequence_jacobian/jacobian/drivers.py | 261 +++++ src/sequence_jacobian/jacobian/support.py | 100 ++ src/sequence_jacobian/models/__init__.py | 1 + src/sequence_jacobian/models/hank.py | 223 +++++ src/sequence_jacobian/models/krusell_smith.py | 125 +++ src/sequence_jacobian/models/rbc.py | 95 ++ src/sequence_jacobian/models/two_asset.py | 423 ++++++++ src/sequence_jacobian/nonlinear.py | 112 +++ src/sequence_jacobian/primitives.py | 137 +++ .../steady_state/__init__.py | 1 + src/sequence_jacobian/steady_state/classes.py | 66 ++ src/sequence_jacobian/steady_state/drivers.py | 306 ++++++ src/sequence_jacobian/steady_state/support.py | 253 +++++ src/sequence_jacobian/utilities/__init__.py | 3 + .../utilities/differentiate.py | 56 ++ src/sequence_jacobian/utilities/discretize.py | 139 +++ .../utilities/forward_step.py | 175 ++++ src/sequence_jacobian/utilities/graph.py | 281 ++++++ .../utilities/interpolate.py | 185 ++++ src/sequence_jacobian/utilities/misc.py | 175 ++++ .../utilities/optimized_routines.py | 45 + src/sequence_jacobian/utilities/solvers.py | 148 +++ .../visualization/__init__.py | 1 + .../visualization/draw_dag.py | 161 +++ 48 files changed, 6705 insertions(+) create mode 100644 src/sequence_jacobian.egg-info/PKG-INFO create mode 100644 src/sequence_jacobian.egg-info/SOURCES.txt create mode 100644 src/sequence_jacobian.egg-info/dependency_links.txt create mode 100644 src/sequence_jacobian.egg-info/requires.txt create mode 100644 src/sequence_jacobian.egg-info/top_level.txt create mode 100644 src/sequence_jacobian/__init__.py create mode 100644 src/sequence_jacobian/blocks/__init__.py create mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py create mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py create mode 100644 src/sequence_jacobian/blocks/combined_block.py create mode 100644 src/sequence_jacobian/blocks/het_block.py create mode 100644 src/sequence_jacobian/blocks/simple_block.py create mode 100644 src/sequence_jacobian/blocks/solved_block.py create mode 100644 src/sequence_jacobian/blocks/support/__init__.py create mode 100644 src/sequence_jacobian/blocks/support/impulse.py create mode 100644 src/sequence_jacobian/blocks/support/simple_displacement.py create mode 100644 src/sequence_jacobian/devtools/__init__.py create mode 100644 src/sequence_jacobian/devtools/analysis.py create mode 100644 src/sequence_jacobian/devtools/debug.py create mode 100644 src/sequence_jacobian/devtools/deprecate.py create mode 100644 src/sequence_jacobian/devtools/upgrade.py create mode 100644 src/sequence_jacobian/estimation.py create mode 100644 src/sequence_jacobian/jacobian/__init__.py create mode 100644 src/sequence_jacobian/jacobian/classes.py create mode 100644 src/sequence_jacobian/jacobian/drivers.py create mode 100644 src/sequence_jacobian/jacobian/support.py create mode 100644 src/sequence_jacobian/models/__init__.py create mode 100644 src/sequence_jacobian/models/hank.py create mode 100644 src/sequence_jacobian/models/krusell_smith.py create mode 100644 src/sequence_jacobian/models/rbc.py create mode 100644 src/sequence_jacobian/models/two_asset.py create mode 100644 src/sequence_jacobian/nonlinear.py create mode 100644 src/sequence_jacobian/primitives.py create mode 100644 src/sequence_jacobian/steady_state/__init__.py create mode 100644 src/sequence_jacobian/steady_state/classes.py create mode 100644 src/sequence_jacobian/steady_state/drivers.py create mode 100644 src/sequence_jacobian/steady_state/support.py create mode 100644 src/sequence_jacobian/utilities/__init__.py create mode 100644 src/sequence_jacobian/utilities/differentiate.py create mode 100644 src/sequence_jacobian/utilities/discretize.py create mode 100644 src/sequence_jacobian/utilities/forward_step.py create mode 100644 src/sequence_jacobian/utilities/graph.py create mode 100644 src/sequence_jacobian/utilities/interpolate.py create mode 100644 src/sequence_jacobian/utilities/misc.py create mode 100644 src/sequence_jacobian/utilities/optimized_routines.py create mode 100644 src/sequence_jacobian/utilities/solvers.py create mode 100644 src/sequence_jacobian/visualization/__init__.py create mode 100644 src/sequence_jacobian/visualization/draw_dag.py diff --git a/src/sequence_jacobian.egg-info/PKG-INFO b/src/sequence_jacobian.egg-info/PKG-INFO new file mode 100644 index 0000000..535e1d1 --- /dev/null +++ b/src/sequence_jacobian.egg-info/PKG-INFO @@ -0,0 +1,69 @@ +Metadata-Version: 2.1 +Name: sequence-jacobian +Version: 0.0.1 +Summary: Sequence-Space Jacobian Methods for Solving and Estimating Heterogeneous Agent Models +Home-page: https://github.com/shade-econ/sequence-jacobian +Author: Michael Cai +Author-email: michaelcai@u.northwestern.edu +License: UNKNOWN +Description: # Sequence-Space Jacobian + + Interactive guide to Auclert, Bardóczy, Rognlie, Straub (2019): + "Using the Sequence-Space Jacobian to Solve and Estimate Heterogeneous-Agent Models". + + [Click here](https://github.com/shade-econ/sequence-jacobian/archive/master.zip) to download all files as a zip. Note: **major update** on July 26, 2019. + + ## 1. Resources + + - [Paper](https://shade-econ.github.io/sequence-jacobian/sequence_jacobian_paper.pdf) + - [Beamer slides](https://shade-econ.github.io/sequence-jacobian/sequence_jacobian_slides.pdf) + - RBC notebook ([html](https://shade-econ.github.io/sequence-jacobian/rbc.html)) ([Jupyter](notebooks/rbc.ipynb)) + - Krusell-Smith notebook ([html](https://shade-econ.github.io/sequence-jacobian/krusell_smith.html)) ([Jupyter](notebooks/krusell_smith.ipynb)) + - One-asset HANK notebook ([html](https://shade-econ.github.io/sequence-jacobian/hank.html)) ([Jupyter](notebooks/hank.ipynb)) + - Two-asset HANK notebook ([html](https://shade-econ.github.io/sequence-jacobian/two_asset.html)) ([Jupyter](notebooks/two_asset.ipynb)) + - HA Jacobian notebook ([html](https://shade-econ.github.io/sequence-jacobian/het_jacobian.html)) ([Jupyter](notebooks/het_jacobian.ipynb)) + + ### 1.1 RBC notebook + + **Warm-up.** Get familiar with solving models in sequence space using our tools. If you don't have Python, + just start by reading the `html` version. If you do, we recommend downloading our code and running the Jupyter notebook directly on your computer. + + ### 1.2. Krusell-Smith notebook + + **The first example.** A comprehensive tutorial in the context of a simple, well-known HA model. Shows how to compute the Jacobian both "by hand" and with our automated tools. Also shows how to calculate second moments and the likelihood function. + + ### 1.3. One-asset HANK notebook + + **The second example.** Generalizes to a more complex model, with a focus on our automated tools to streamline the workflow. Introduces our winding number criterion for local determinacy. + + ### 1.4. Two-asset HANK notebook + + **The third example.** Showcases the workflow for solving a state-of-the-art HANK model where households hold liquid and illiquid assets, and there are sticky prices, sticky wages, and capital adjustment costs on the production side. Introduces the concept of solved blocks. + + ### 1.5. HA Jacobian notebook + + **Inside the black box.** A step-by-step examination of our fake news algorithm to compute Jacobians of HA blocks. + + ## 2. Setting up Python + + To install a full distribution of Python, with all of the packages and tools you will need to run our code, + download the latest [Python 3 Anaconda](https://www.anaconda.com/distribution/) distribution. + **Note:** make sure you choose the installer for Python version 3. + Once you install Anaconda, you will be able to play with the notebooks we provided. Just open a terminal, change + directory to the folder with notebooks, and type `jupyter notebook`. This will launch the notebook dashboard in your + default browser. Click on a notebook to get started. + + For more information on Jupyter notebooks, check out the + [official quick start guide](https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/). + If you'd like to learn more about Python, the [QuantEcon](https://lectures.quantecon.org/py/) lectures of + Tom Sargent and John Stachurski are a great place to start. + + + + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.7 +Description-Content-Type: text/markdown diff --git a/src/sequence_jacobian.egg-info/SOURCES.txt b/src/sequence_jacobian.egg-info/SOURCES.txt new file mode 100644 index 0000000..98ef412 --- /dev/null +++ b/src/sequence_jacobian.egg-info/SOURCES.txt @@ -0,0 +1,7 @@ +README.md +setup.py +src/sequence_jacobian.egg-info/PKG-INFO +src/sequence_jacobian.egg-info/SOURCES.txt +src/sequence_jacobian.egg-info/dependency_links.txt +src/sequence_jacobian.egg-info/requires.txt +src/sequence_jacobian.egg-info/top_level.txt \ No newline at end of file diff --git a/src/sequence_jacobian.egg-info/dependency_links.txt b/src/sequence_jacobian.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/sequence_jacobian.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/sequence_jacobian.egg-info/requires.txt b/src/sequence_jacobian.egg-info/requires.txt new file mode 100644 index 0000000..1f49b43 --- /dev/null +++ b/src/sequence_jacobian.egg-info/requires.txt @@ -0,0 +1,4 @@ +numpy>=1.18 +scipy>=1.5 +numba>=0.50 +xarray>=0.17 diff --git a/src/sequence_jacobian.egg-info/top_level.txt b/src/sequence_jacobian.egg-info/top_level.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/sequence_jacobian.egg-info/top_level.txt @@ -0,0 +1 @@ + diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py new file mode 100644 index 0000000..2f7abb6 --- /dev/null +++ b/src/sequence_jacobian/__init__.py @@ -0,0 +1,31 @@ +"""Public-facing objects.""" + +from . import estimation, jacobian, nonlinear, utilities, devtools + +from .models import rbc, krusell_smith, hank, two_asset + +from .blocks.simple_block import simple +from .blocks.het_block import het, hetoutput +from .blocks.solved_block import solved +from .blocks.combined_block import combine, create_model +from .blocks.support.simple_displacement import apply_function + +from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved + +from .steady_state.drivers import steady_state +from .jacobian.drivers import get_G, get_H_U, get_impulse +from .nonlinear import td_solve + +# Useful utilities for setting up HetBlocks +from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen +from .utilities.interpolate import interpolate_y +from .utilities.optimized_routines import setmin + +# Ensure warning uniformity across package +import warnings + +# Force warnings.warn() to omit the source code line in the message +formatwarning_orig = warnings.formatwarning +warnings.formatwarning = lambda message, category, filename, lineno, line=None: \ + formatwarning_orig(message, category, filename, lineno, line='') + diff --git a/src/sequence_jacobian/blocks/__init__.py b/src/sequence_jacobian/blocks/__init__.py new file mode 100644 index 0000000..b33d734 --- /dev/null +++ b/src/sequence_jacobian/blocks/__init__.py @@ -0,0 +1 @@ +"""Block-construction tools""" \ No newline at end of file diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py b/src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py new file mode 100644 index 0000000..d4a7666 --- /dev/null +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/__init__.py @@ -0,0 +1 @@ +"""Auxiliary Block types for building a coherent backend for Block handling""" diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py new file mode 100644 index 0000000..227779d --- /dev/null +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -0,0 +1,26 @@ +"""A simple wrapper for JacobianDicts to be embedded in DAGs""" + +from numbers import Real +from typing import Dict, Union, List + +from ...primitives import Block, Array +from ...jacobian.classes import JacobianDict + + +class JacobianDictBlock(JacobianDict, Block): + """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) + + def __repr__(self): + return f"" + + def impulse_linear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: + return self.jacobian(list(exogenous.keys())).apply(exogenous) + + def jacobian(self, exogenous: List[str] = None, **kwargs) -> JacobianDict: + if exogenous is None: + return JacobianDict(self.nesteddict) + else: + return self[:, exogenous] diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py new file mode 100644 index 0000000..3835b1f --- /dev/null +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -0,0 +1,131 @@ +"""CombinedBlock class and the combine function to generate it""" + +from copy import deepcopy + +from .support.impulse import ImpulseDict +from ..primitives import Block +from .. import utilities as utils +from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock +from ..steady_state.drivers import eval_block_ss +from ..steady_state.support import provide_solver_default +from ..jacobian.classes import JacobianDict + + +def combine(blocks, name="", model_alias=False): + return CombinedBlock(blocks, name=name, model_alias=model_alias) + + +# Useful functional alias +def create_model(blocks, **kwargs): + return combine(blocks, model_alias=True, **kwargs) + + +class CombinedBlock(Block): + """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides + a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, + and calculates Jacobians along the DAG""" + # To users: Do *not* manually change the attributes via assignment. Instantiating a + # CombinedBlock has some automated features that are inferred from initial instantiation but not from + # re-assignment of attributes post-instantiation. + def __init__(self, blocks, name="", model_alias=False): + + self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] + self._sorted_indices = utils.graph.block_sort(blocks) + self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] + + if not name: + self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" + else: + self.name = name + + # Find all outputs (including those used as intermediary inputs) + self.outputs = set().union(*[block.outputs for block in self.blocks]) + + # Find all inputs that are *not* intermediary outputs + all_inputs = set().union(*[block.inputs for block in self.blocks]) + self.inputs = all_inputs.difference(self.outputs) + + # If the create_model() is used instead of combine(), we will have __repr__ show this object as a 'Model' + self._model_alias = model_alias + + def __repr__(self): + if self._model_alias: + return f"" + else: + return f"" + + def steady_state(self, calibration, helper_blocks=None, **kwargs): + """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" + if helper_blocks is None: + helper_blocks = [] + + topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) + blocks_all = self.blocks + helper_blocks + + ss_partial_eq = deepcopy(calibration) + for i in topsorted: + ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) + return ss_partial_eq + + def impulse_nonlinear(self, ss, exogenous, **kwargs): + """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from + a steady state, `ss`""" + irf_nonlin_partial_eq = deepcopy(exogenous) + for block in self.blocks: + input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} + + if input_args: # If this block is actually perturbed + irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) + + return ImpulseDict(irf_nonlin_partial_eq) + + def impulse_linear(self, ss, exogenous, T=None, Js=None): + """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from + a steady_state, `ss`""" + irf_lin_partial_eq = deepcopy(exogenous) + for block in self.blocks: + input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} + + if input_args: # If this block is actually perturbed + irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T, Js=Js)}) + + return ImpulseDict(irf_lin_partial_eq) + + def jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): + """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at + a steady state, `ss`""" + if exogenous is None: + exogenous = list(self.inputs) + if outputs is None: + outputs = self.outputs + kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "Js": Js} + + for i, block in enumerate(self.blocks): + curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() + + # If we want specific list of outputs, restrict curlyJ to that before continuing + curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] + if i == 0: + J_partial_eq = curlyJ.compose(JacobianDict.identity(exogenous)) + else: + J_partial_eq.update(curlyJ.compose(J_partial_eq)) + + return J_partial_eq + + def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, + sort_blocks=False, **kwargs): + """Evaluate a general equilibrium steady state of the CombinedBlock given a `calibration` + and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and + the target conditions that must hold in general equilibrium""" + if solver is None: + solver = provide_solver_default(unknowns) + if helper_blocks and sort_blocks is False: + sort_blocks = True + + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, + helper_blocks=helper_blocks, sort_blocks=sort_blocks, **kwargs) + + +# Useful type aliases +Model = CombinedBlock diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py new file mode 100644 index 0000000..9a2c89d --- /dev/null +++ b/src/sequence_jacobian/blocks/het_block.py @@ -0,0 +1,922 @@ +import warnings +import copy +import numpy as np + +from .support.impulse import ImpulseDict +from ..primitives import Block +from .. import utilities as utils +from ..steady_state.classes import SteadyStateDict +from ..jacobian.classes import JacobianDict +from ..devtools.deprecate import rename_output_list_to_outputs +from ..utilities.misc import verify_saved_jacobian + + +def het(exogenous, policy, backward, backward_init=None): + def decorator(back_step_fun): + return HetBlock(back_step_fun, exogenous, policy, backward, backward_init=backward_init) + return decorator + + +class HetBlock(Block): + """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. + + IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since + the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle + aggregation of non-aggregate outputs across the distribution and return aggregates as upper-case equivalents + of the `policy` and non-aggregate output variables specified in the backward step function. + """ + + def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None): + """Construct HetBlock from backward iteration function. + + Parameters + ---------- + back_step_fun : function + backward iteration function + exogenous : str + name of Markov transition matrix for exogenous variable + (now only single allowed for simplicity; use Kronecker product for more) + policy : str or sequence of str + names of policy variables of endogenous, continuous state variables + e.g. assets 'a', must be returned by function + backward : str or sequence of str + variables that together comprise the 'v' that we use for iterating backward + must appear both as outputs and as arguments + + It is assumed that every output of the function (except possibly backward), including policy, + will be on a grid of dimension 1 + len(policy), where the first dimension is the exogenous + variable and then the remaining dimensions are each of the continuous policy variables, in the + same order they are listed in 'policy'. + + The Markov transition matrix between the current and future period and backward iteration + variables should appear in the backward iteration function with '_p' subscripts ("prime") to + indicate that they come from the next period. + + Currently, we only support up to two policy variables. + """ + self.name = back_step_fun.__name__ + + # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. + # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) + self.back_step_fun = back_step_fun + + # self.back_step_outputs and self.back_step_inputs are all of the output and input arguments of + # self.back_step_fun, the variables used in the backward iteration, + # which generally include value and/or policy functions. + self.back_step_output_list = utils.misc.output_list(back_step_fun) + self.back_step_outputs = set(self.back_step_output_list) + self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) + + # See the docstring of HetBlock for details on the attributes directly below + self.exogenous = exogenous + self.policy, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (policy, backward)) + + # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" + # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun + # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to + # the primed key name, such that self.back_step_fun will properly call those variables. + # e.g. the key "Va" will become "Va_p", associated to the same value. + self.inputs_to_be_primed = {self.exogenous} | set(self.back_iter_vars) + + # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward + # iteration variables themselves. + self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) + + # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used + # in utils.graph.block_sort to topologically sort blocks along the DAG + # according to their aggregate outputs and inputs. + self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} + self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} + self.inputs.remove(exogenous + '_p') + self.inputs.add(exogenous) + + # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. + # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. + self.hetinput = None + self.hetinput_inputs = set() + self.hetinput_outputs = set() + self.hetinput_outputs_order = tuple() + + # start without a hetoutput + self.hetoutput = None + self.hetoutput_inputs = set() + self.hetoutput_outputs = set() + self.hetoutput_outputs_order = tuple() + + # The set of variables that will be wrapped in a separate namespace for this HetBlock + # as opposed to being available at the top level + self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} + + if len(self.policy) > 2: + raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") + + # Checking that the various inputs/outputs attributes are correctly set + if self.exogenous + '_p' not in self.back_step_inputs: + raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") + + for pol in self.policy: + if pol not in self.back_step_outputs: + raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") + if pol[0].isupper(): + raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") + + for back in self.back_iter_vars: + if back + '_p' not in self.back_step_inputs: + raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") + + if back not in self.back_step_outputs: + raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") + + for out in self.non_back_iter_outputs: + if out[0].isupper(): + raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") + + # Add the backward iteration initializer function (the initial guesses for self.back_iter_vars) + if backward_init is None: + # TODO: Think about implementing some "automated way" of providing + # an initial guess for the backward iteration. + self.backward_init = backward_init + else: + self.backward_init = backward_init + + # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. + + def __repr__(self): + """Nice string representation of HetBlock for printing to console""" + if self.hetinput is not None: + if self.hetoutput is not None: + return f"" + else: + return f"" + else: + return f"" + + '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts + - ss : do backward and forward iteration until convergence to get complete steady state + - td : do backward and forward iteration up to T to compute dynamics given some shocks + - jac : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm + - ajac : compute asymptotic columns of jacobians output by jac, also using fake news algorithm + + - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput + - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after + each backward iteration step in td + ''' + + # TODO: Deprecated methods, to be removed! + def ss(self, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(kwargs) + + def td(self, ss, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(ss, **kwargs) + + def jac(self, ss, shock_list=None, T=None, **kwargs): + if shock_list is None: + shock_list = list(self.inputs) + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, shock_list, T, **kwargs) + + def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, + forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): + """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. + + Parameters + ---------- + backward_tol : [optional] float + in backward iteration, max abs diff between policy in consecutive steps needed for convergence + backward_maxit : [optional] int + maximum number of backward iterations, if 'backward_tol' not reached by then, raise error + forward_tol : [optional] float + in forward iteration, max abs diff between dist in consecutive steps needed for convergence + forward_maxit : [optional] int + maximum number of forward iterations, if 'forward_tol' not reached by then, raise error + + kwargs : dict + The following inputs are required as keyword arguments, which show up in 'kwargs': + - The exogenous Markov matrix, e.g. Pi=... if self.exogenous=='Pi' + - A seed for each backward variable, e.g. Va=... and Vb=... if self.back_iter_vars==('Va','Vb') + - A grid for each policy variable, e.g. a_grid=... and b_grid=... if self.policy==('a','b') + - All other inputs to the backward iteration function self.back_step_fun, except _p added to + for self.exogenous and self.back_iter_vars, for which the method uses steady-state values. + If there is a self.hetinput, then we need the inputs to that, not to self.back_step_fun. + + Other inputs in 'kwargs' are optional: + - A seed for the distribution: D=... + - If no seed for the distribution is provided, a seed for the invariant distribution + of the Markov process, e.g. Pi_seed=... if self.exogenous=='Pi' + + Returns + ---------- + ss : dict, contains + - ss inputs of self.back_step_fun and (if present) self.hetinput + - ss outputs of self.back_step_fun + - ss distribution 'D' + - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars + """ + + ss = copy.deepcopy(calibration) + + # extract information from calibration + Pi = calibration[self.exogenous] + grid = {k: calibration[k+'_grid'] for k in self.policy} + D_seed = calibration.get('D', None) + pi_seed = calibration.get(self.exogenous + '_seed', None) + + # run backward iteration + sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) + ss.update(sspol) + + # run forward iteration + D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) + ss.update({"D": D}) + + # aggregate all outputs other than backward variables on grid, capitalize + aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} + ss.update(aggregates) + + if hetoutput and self.hetoutput is not None: + hetoutputs = self.hetoutput.evaluate(ss) + aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") + else: + hetoutputs = {} + aggregate_hetoutputs = {} + ss.update({**hetoutputs, **aggregate_hetoutputs}) + + return SteadyStateDict(ss, internal=self) + + def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): + """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, + assuming that we start and end in steady state ss, and that all inputs not specified in + kwargs are constant at their ss values. Analog to SimpleBlock.td. + + CANNOT provide time-varying paths of grid or Markov transition matrix for now. + + Parameters + ---------- + ss : SteadyStateDict + all steady-state info, intended to be from .ss() + exogenous : dict of {str : array(T, ...)} + all time-varying inputs here (in deviations), with first dimension being time + this must have same length T for all entries (all outputs will be calculated up to T) + monotonic : [optional] bool + flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us + to use faster interpolation routines, otherwise use slower robust to nonmonotonicity + returnindividual : [optional] bool + return distribution and full outputs on grid + grid_paths: [optional] dict of {str: array(T, Number of grid points)} + time-varying grids for policies + + Returns + ---------- + td : dict + if returnindividual = False, time paths for aggregates (uppercase) for all outputs + of self.back_step_fun except self.back_iter_vars + if returnindividual = True, additionally time paths for distribution and for all outputs + of self.back_Step_fun on the full grid + """ + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + + # copy from ss info + Pi_T = ss[self.exogenous].T.copy() + D = ss.internal[self.name]['D'] + + # construct grids for policy variables either from the steady state grid if the grid is meant to be + # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. + grid = {} + use_ss_grid = {} + for k in self.policy: + if grid_paths is not None and k in grid_paths: + grid[k] = grid_paths[k] + use_ss_grid[k] = False + else: + grid[k] = ss[k+"_grid"] + use_ss_grid[k] = True + + # allocate empty arrays to store result, assume all like D + individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} + hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} + + # backward iteration + backdict = dict(ss.items()) + backdict.update(copy.deepcopy(ss.internal[self.name])) + for t in reversed(range(T)): + # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! + backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + individual = {k: v for k, v in zip(self.back_step_output_list, + self.back_step_fun(**self.make_inputs(backdict)))} + backdict.update({k: individual[k] for k in self.back_iter_vars}) + + if self.hetoutput is not None: + hetoutput = self.hetoutput.evaluate(backdict) + for k in self.hetoutput_outputs: + hetoutput_paths[k][t, ...] = hetoutput[k] + + for k in self.non_back_iter_outputs: + individual_paths[k][t, ...] = individual[k] + + D_path = np.empty((T,) + D.shape) + D_path[0, ...] = D + for t in range(T-1): + # have to interpolate policy separately for each t to get sparse transition matrices + sspol_i = {} + sspol_pi = {} + for pol in self.policy: + if use_ss_grid[pol]: + grid_var = grid[pol] + else: + grid_var = grid[pol][t, ...] + if monotonic: + # TODO: change for two-asset case so assumption is monotonicity in own asset, not anything else + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord(grid_var, + individual_paths[pol][t, ...]) + else: + sspol_i[pol], sspol_pi[pol] =\ + utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[pol][t, ...]) + + # step forward + D_path[t+1, ...] = self.forward_step(D_path[t, ...], Pi_T, sspol_i, sspol_pi) + + # obtain aggregates of all outputs, made uppercase + aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) + for o in self.non_back_iter_outputs} + if self.hetoutput: + aggregate_hetoutputs = self.hetoutput.aggregate(hetoutput_paths, D_path, backdict, mode="td") + else: + aggregate_hetoutputs = {} + + # return either this, or also include distributional information + if returnindividual: + return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, + 'D': D_path}) - ss + else: + return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss + + def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + + return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) + + def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): + """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. + + Parameters + ---------- + ss : dict, + all steady-state info, intended to be from .ss() + T : [optional] int + number of time periods for T*T Jacobian + exogenous : list of str + names of input variables to differentiate wrt (main cost scales with # of inputs) + outputs : list of str + names of output variables to get derivatives of, if not provided assume all outputs of + self.back_step_fun except self.back_iter_vars + h : [optional] float + h for numerical differentiation of backward iteration + Js : [optional] dict of {str: JacobianDict}} + supply saved Jacobians + + Returns + ------- + J : dict of {str: dict of {str: array(T,T)}} + J[o][i] for output o and input i gives T*T Jacobian of o with respect to i + """ + # The default set of outputs are all outputs of the backward iteration function + # except for the backward iteration variables themselves + if exogenous is None: + exogenous = list(self.inputs) + if outputs is None or output_list is None: + outputs = self.non_back_iter_outputs + else: + outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) + + relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] + + # if we supply Jacobians, use them if possible, warn if they cannot be used + if Js is not None: + outputs_cap = [o.capitalize() for o in outputs] + if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): + return Js[self.name] + + # step 0: preliminary processing of steady state + (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) + + # step 1 of fake news algorithm + # compute curlyY and curlyD (backward iteration) for each input i + curlyYs, curlyDs = {}, {} + for i in relevant_shocks: + curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, + ss.internal[self.name]['D'], Pi.T.copy(), + sspol_i, sspol_pi, sspol_space, T, h, + ss_for_hetinput) + + # step 2 of fake news algorithm + # compute prediction vectors curlyP (forward iteration) for each outcome o + curlyPs = {} + for o in outputs: + curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) + + # steps 3-4 of fake news algorithm + # make fake news matrix and Jacobian for each outcome-input pair + F, J = {}, {} + for o in outputs: + for i in relevant_shocks: + if o.capitalize() not in F: + F[o.capitalize()] = {} + if o.capitalize() not in J: + J[o.capitalize()] = {} + F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) + J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) + + return JacobianDict(J, name=self.name) + + def add_hetinput(self, hetinput, overwrite=False, verbose=True): + """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process + inputs through the hetinput function. + + A `hetinput` is any non-scalar-valued input argument provided to the HetBlock's backward iteration function, + self.back_step_fun, which is of the same dimensions as the distribution of agents in the HetBlock over + the relevant idiosyncratic state variables, generally referred to as `D`. e.g. The one asset HANK model + example provided in the models directory of sequence_jacobian has a hetinput `T`, which is skill-specific + transfers. + """ + if self.hetinput is not None and overwrite is False: + raise ValueError('Trying to attach hetinput when one already exists!') + else: + if verbose: + if self.hetinput is not None and overwrite is True: + print(f"Overwriting current hetinput, {self.hetinput.__name__} with new hetinput," + f" {hetinput.__name__}!") + else: + print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") + + self.hetinput = hetinput + self.hetinput_inputs = set(utils.misc.input_list(hetinput)) + self.hetinput_outputs = set(utils.misc.output_list(hetinput)) + self.hetinput_outputs_order = utils.misc.output_list(hetinput) + + # modify inputs to include hetinput's additional inputs, remove outputs + self.inputs |= self.hetinput_inputs + self.inputs -= self.hetinput_outputs + + self.internal |= self.hetinput_outputs + + def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): + """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process + inputs through the hetoutput function. + + A `hetoutput` is any *non-scalar-value* output that the user might desire to be calculated from + the output arguments of the HetBlock's backward iteration function. Importantly, as of now the `hetoutput` + cannot be a function of time displaced values of the HetBlock's outputs but rather must be able to + be calculated from the outputs statically. e.g. The two asset HANK model example provided in the models + directory of sequence_jacobian has a hetoutput, `chi`, the adjustment costs for any initial level of assets + `a`, to any new level of assets `a'`. + """ + if self.hetoutput is not None and overwrite is False: + raise ValueError('Trying to attach hetoutput when one already exists!') + else: + if verbose: + if self.hetoutput is not None and overwrite is True: + print(f"Overwriting current hetoutput, {self.hetoutput.name} with new hetoutput," + f" {hetoutput.name}!") + else: + print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") + + self.hetoutput = hetoutput + self.hetoutput_inputs = set(hetoutput.input_list) + self.hetoutput_outputs = set(hetoutput.output_list) + self.hetoutput_outputs_order = hetoutput.output_list + + # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput + # and aggregating the hetoutput, but do not include: + # 1) objects computed within the HetBlock's backward iteration that enter into the hetoutput computation + # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) + # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation + # but is computed after the backward iteration + self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) + # Modify the HetBlock's outputs to include the aggregated hetoutputs + self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) + + self.internal |= self.hetoutput_outputs + + '''Part 3: components of ss(): + - policy_ss : backward iteration to get steady-state policies and other outcomes + - dist_ss : forward iteration to get steady-state distribution and compute aggregates + ''' + + def policy_ss(self, ssin, tol=1E-8, maxit=5000): + """Find steady-state policies and backward variables through backward iteration until convergence. + + Parameters + ---------- + ssin : dict + all steady-state inputs to back_step_fun, including seed values for backward variables + tol : [optional] float + max diff between consecutive iterations of policy variables needed for convergence + maxit : [optional] int + maximum number of iterations, if 'tol' not reached by then, raise error + + Returns + ---------- + sspol : dict + all steady-state outputs of backward iteration, combined with inputs to backward iteration + """ + + # find initial values for backward iteration and account for hetinputs + original_ssin = ssin + ssin = self.make_inputs(ssin) + + old = {} + for it in range(maxit): + try: + # run and store results of backward iteration, which come as tuple, in dict + sspol = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**ssin))} + except KeyError as e: + print(f'Missing input {e} to {self.back_step_fun.__name__}!') + raise + + # only check convergence every 10 iterations for efficiency + if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) + for k in self.policy): + break + + # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration + old.update({k: sspol[k] for k in self.policy}) + ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) + else: + raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') + + # want to record inputs in ssin, but remove _p, add in hetinput inputs if there + for k in self.inputs_to_be_primed: + ssin[k] = ssin[k + '_p'] + del ssin[k + '_p'] + if self.hetinput is not None: + for k in self.hetinput_inputs: + if k in original_ssin: + ssin[k] = original_ssin[k] + return {**ssin, **sspol} + + def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): + """Find steady-state distribution through forward iteration until convergence. + + Parameters + ---------- + Pi : array + steady-state Markov matrix for exogenous variable + sspol : dict + steady-state policies on grid for all policy variables in self.policy + grid : dict + grids for all policy variables in self.policy + tol : [optional] float + absolute tolerance for max diff between consecutive iterations for distribution + maxit : [optional] int + maximum number of iterations, if 'tol' not reached by then, raise error + D_seed : [optional] array + initial seed for overall distribution + pi_seed : [optional] array + initial seed for stationary dist of Pi, if no D_seed + + Returns + ---------- + D : array + steady-state distribution + """ + + # first obtain initial distribution D + if D_seed is None: + # compute stationary distribution for exogenous variable + pi = utils.discretize.stationary(Pi, pi_seed) + + # now initialize full distribution with this, assuming uniform distribution on endogenous vars + endogenous_dims = [grid[k].shape[0] for k in self.policy] + D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) + else: + D = D_seed + + # obtain interpolated policy rule for each dimension of endogenous policy + sspol_i = {} + sspol_pi = {} + for pol in self.policy: + # use robust binary search-based method that only requires grids, not policies, to be monotonic + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], sspol[pol]) + + # iterate until convergence by tol, or maxit + Pi_T = Pi.T.copy() + for it in range(maxit): + Dnew = self.forward_step(D, Pi_T, sspol_i, sspol_pi) + + # only check convergence every 10 iterations for efficiency + if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): + break + D = Dnew + else: + raise ValueError(f'No convergence after {maxit} forward iterations!') + + return D + + '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper + - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs + - Step 2: forward_iteration_fakenews to get curlyPs + - Step 3: build_F to get fake news matrix from curlyYs, curlyDs, curlyPs + - Step 4: J_from_F to get Jacobian from fake news matrix + ''' + + def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): + # shock perturbs outputs + shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, + utils.differentiate.numerical_diff(self.back_step_fun, + ssin_dict, din_dict, h, + ssout_list))} + curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} + + # which affects the distribution tomorrow + pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} + + # Include an additional term to account for the effect of a deleveraging shock affecting the grid + if "delev_exante" in din_dict: + dx = np.zeros_like(sspol_pi["a"]) + dx[sspol_i["a"] == 0] = 1. + add_term = sspol_pi["a"] * dx / sspol_space["a"] + pol_pi_shock["a"] += add_term + + curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) + + # and the aggregate outcomes today + curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} + + return curlyV, curlyD, curlyY + + def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, + sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): + """Iterate policy steps backward T times for a single shock.""" + # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None + # since unless self.hetinput_inputs is exactly equal to input_shocked, calling + # self.hetinput() inside the symmetric differentiation function will throw an error. + # It's probably better/more informative to throw that error out here. + if self.hetinput is not None and input_shocked in self.hetinput_inputs: + # if input_shocked is an input to hetinput, take numerical diff to get response + din_dict = dict(zip(self.hetinput_outputs_order, + utils.differentiate.numerical_diff_symmetric(self.hetinput, + ss_for_hetinput, {input_shocked: 1}, h))) + else: + # otherwise, we just have that one shock + din_dict = {input_shocked: 1} + + # contemporaneous response to unit scalar shock + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) + + # infer dimensions from this and initialize empty arrays + curlyDs = np.empty((T,) + curlyD.shape) + curlyYs = {k: np.empty(T) for k in curlyY.keys()} + + # fill in current effect of shock + curlyDs[0, ...] = curlyD + for k in curlyY.keys(): + curlyYs[k][0] = curlyY[k] + + # fill in anticipation effects + for t in range(1, T): + curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, + output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) + for k in curlyY.keys(): + curlyYs[k][t] = curlyY[k] + + return curlyYs, curlyDs + + def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): + """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. + + Note we depart from definition in paper by applying the demeaning operator in addition to Lambda + at each step. This does not affect products with curlyD (which are the only way curlyPs enter + Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits + since curlyPs now go to zero for high t (used in paper in proof of Proposition 1). + """ + curlyPs = np.empty((T,) + o_ss.shape) + curlyPs[0, ...] = utils.misc.demean(o_ss) + for t in range(1, T): + curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], + Pi, pol_i_ss, pol_pi_ss)) + return curlyPs + + @staticmethod + def build_F(curlyYs, curlyDs, curlyPs): + T = curlyDs.shape[0] + Tpost = curlyPs.shape[0] - T + 2 + F = np.empty((Tpost + T - 1, T)) + F[0, :] = curlyYs + F[1:, :] = curlyPs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T + return F + + @staticmethod + def J_from_F(F): + J = F.copy() + for t in range(1, J.shape[1]): + J[1:, t] += J[:-1, t - 1] + return J + + '''Part 5: helpers for .jac and .ajac: preliminary processing''' + + def jac_prelim(self, ss): + """Helper that does preliminary processing of steady state for fake news algorithm. + + Parameters + ---------- + ss : dict, all steady-state info, intended to be from .ss() + + Returns + ---------- + ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step + Pi : array (S*S), Markov matrix for exogenous state + ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same + as steady-state numerically since SS convergence was to some tolerance threshold) + ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) + sspol_i : dict, indices on lower bracketing gridpoint for all in self.policy + sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy + sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy + """ + # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation + ssin_dict = self.make_inputs(ss) + Pi = ss[self.exogenous] + grid = {k: ss[k+'_grid'] for k in self.policy} + ssout_list = self.back_step_fun(**ssin_dict) + + ss_for_hetinput = None + if self.hetinput is not None: + ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} + + # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints + sspol_i = {} + sspol_pi = {} + sspol_space = {} + for pol in self.policy: + # use robust binary-search-based method that only requires grids to be monotonic + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) + sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] + + toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) + + return toreturn + + '''Part 6: helper to extract inputs and potentially process them through hetinput''' + + def make_inputs(self, back_step_inputs_dict): + """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, + process stuff through self.hetinput first if it's there. + """ + input_dict = copy.deepcopy(back_step_inputs_dict) + + # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady + # state computation as well as for Jacobian/impulse evaluation for HetBlocks, + # where in the former the hetinputs and value function have yet to be computed, + # whereas in the latter they have already been computed + # and hence do not need to be recomputed. There may be room to clean this function up a bit. + if isinstance(back_step_inputs_dict, SteadyStateDict): + input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) + input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) + else: + # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include + # them as inputs for self.back_step_fun + if self.hetinput is not None: + outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] + for k in self.hetinput_inputs if k in input_dict})) + input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) + + # Check if there are entries in indict corresponding to self.inputs_to_be_primed. + # In particular, we are interested in knowing if an initial value + # for the backward iteration variable has been provided. + # If it has not been provided, then use self.backward_init to calculate the initial values. + if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): + initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] + input_dict.update(zip(utils.misc.output_list(self.backward_init), + utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) + + for i_p in self.inputs_to_be_primed: + input_dict[i_p + "_p"] = input_dict[i_p] + del input_dict[i_p] + + try: + return {k: input_dict[k] for k in self.back_step_inputs if k in input_dict} + except KeyError as e: + print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') + raise + + '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' + + def forward_step(self, D, Pi_T, pol_i, pol_pi): + """Update distribution, calling on 1d and 2d-specific compiled routines. + + Parameters + ---------- + D : array, beginning-of-period distribution + Pi_T : array, transpose Markov matrix + pol_i : dict, indices on lower bracketing gridpoint for all in self.policy + pol_pi : dict, weights on lower bracketing gridpoint for all in self.policy + + Returns + ---------- + Dnew : array, beginning-of-next-period distribution + """ + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_1d(D, Pi_T, pol_i[p], pol_pi[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_2d(D, Pi_T, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + def forward_step_transpose(self, D, Pi, pol_i, pol_pi): + """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_transpose_1d(D, Pi, pol_i[p], pol_pi[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_transpose_2d(D, Pi, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): + """Forward_step linearized with respect to pol_pi""" + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_shock_1d(Dss, Pi_T, pol_i_ss[p], pol_pi_shock[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_shock_2d(Dss, Pi_T, pol_i_ss[p1], pol_i_ss[p2], + pol_pi_ss[p1], pol_pi_ss[p2], + pol_pi_shock[p1], pol_pi_shock[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + +def hetoutput(custom_aggregation=None): + def decorator(f): + return HetOutput(f, custom_aggregation=custom_aggregation) + return decorator + + +class HetOutput: + def __init__(self, f, custom_aggregation=None): + self.name = f.__name__ + self.f = f + self.eval_input_list = utils.misc.input_list(f) + + self.custom_aggregation = custom_aggregation + self.agg_input_list = [] if custom_aggregation is None else utils.misc.input_list(custom_aggregation) + + # We are distinguishing between the eval_input_list and agg_input_list because custom aggregation may require + # certain arguments that are not required for simply evaluating the hetoutput + self.input_list = list(set(self.eval_input_list).union(set(self.agg_input_list))) + self.output_list = utils.misc.output_list(f) + + def evaluate(self, arg_dict): + hetoutputs = dict(zip(self.output_list, utils.misc.make_tuple(self.f(*[arg_dict[i] for i + in self.eval_input_list])))) + return hetoutputs + + def aggregate(self, hetoutputs, D, custom_aggregation_args, mode="ss"): + if self.custom_aggregation is not None: + hetoutputs_w_std_aggregation = list(set(self.output_list) - + set([utils.misc.uncapitalize(o) for o + in utils.misc.output_list(self.custom_aggregation)])) + hetoutputs_w_custom_aggregation = list(set(self.output_list) - set(hetoutputs_w_std_aggregation)) + else: + hetoutputs_w_std_aggregation = self.output_list + hetoutputs_w_custom_aggregation = [] + + # TODO: May need to check if this works properly for td + if self.custom_aggregation is not None: + hetoutputs_w_custom_aggregation_args = dict(zip(hetoutputs_w_custom_aggregation, + [hetoutputs[i] for i in hetoutputs_w_custom_aggregation])) + custom_agg_inputs = {"D": D, **hetoutputs_w_custom_aggregation_args, **custom_aggregation_args} + custom_aggregates = dict(zip([o.capitalize() for o in hetoutputs_w_custom_aggregation], + utils.misc.make_tuple(self.custom_aggregation(*[custom_agg_inputs[i] for i + in self.agg_input_list])))) + else: + custom_aggregates = {} + + if mode == "ss": + std_aggregates = {o.capitalize(): np.vdot(D, hetoutputs[o]) for o in hetoutputs_w_std_aggregation} + elif mode == "td": + std_aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D, hetoutputs[o]) + for o in hetoutputs_w_std_aggregation} + else: + raise RuntimeError(f"Mode {mode} is not supported in HetOutput aggregation. Choose either 'ss' or 'td'") + + return {**std_aggregates, **custom_aggregates} diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py new file mode 100644 index 0000000..c323e2d --- /dev/null +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -0,0 +1,170 @@ +"""Class definition of a simple block""" + +import warnings +import numpy as np +from copy import deepcopy + +from .support.simple_displacement import ignore, Displace, AccumulatedDerivative +from .support.impulse import ImpulseDict +from ..primitives import Block +from ..steady_state.classes import SteadyStateDict +from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix +from ..utilities import misc + +'''Part 1: SimpleBlock class and @simple decorator to generate it''' + + +def simple(f): + return SimpleBlock(f) + + +class SimpleBlock(Block): + """Generated from simple block written in Dynare-ish style and decorated with @simple, e.g. + + @simple + def production(Z, K, L, alpha): + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return Y + + which is a SimpleBlock that takes in Z, K, L, and alpha, all of which can be either constants + or series, and implements a Cobb-Douglas production function, noting that for production today + we use the capital K(-1) determined yesterday. + + Key methods are .ss, .td, and .jac, like HetBlock. + """ + + def __init__(self, f): + self.f = f + self.name = f.__name__ + self.input_list = misc.input_list(f) + self.output_list = misc.output_list(f) + self.inputs = set(self.input_list) + self.outputs = set(self.output_list) + + def __repr__(self): + return f"" + + # TODO: Deprecated methods, to be removed! + def ss(self, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(kwargs) + + def td(self, ss, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(ss, exogenous=kwargs) + + def jac(self, ss, T=None, shock_list=None): + if shock_list is None: + shock_list = [] + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, exogenous=shock_list, T=T) + + def steady_state(self, calibration): + input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} + output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ + misc.numeric_primitive(self.f(**input_args))] + return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) + + def impulse_nonlinear(self, ss, exogenous): + input_args = {} + for k, v in exogenous.items(): + if np.isscalar(v): + raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') + input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) + + for k in self.input_list: + if k not in input_args: + input_args[k] = ignore(ss[k]) + + return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) - ss + + def impulse_linear(self, ss, exogenous, T=None, Js=None): + return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) + + def jacobian(self, ss, exogenous=None, T=None, Js=None): + """Assemble nested dict of Jacobians + + Parameters + ---------- + ss : dict, + steady state values + exogenous : list of str, optional + names of input variables to differentiate wrt; if omitted, assume all inputs + T : int, optional + number of time periods for explicit T*T Jacobian + if omitted, more efficient SimpleSparse objects returned + Js : dict of {str: JacobianDict}, optional + pre-computed Jacobians + + Returns + ------- + J : dict of {str: dict of {str: array(T,T)}} + J[o][i] for output o and input i gives Jacobian of o with respect to i + This Jacobian is a SimpleSparse object or, if T specific, a T*T matrix, omitted by convention if zero + """ + + if exogenous is None: + exogenous = list(self.inputs) + + relevant_shocks = [i for i in self.inputs if i in exogenous] + + # if we supply Jacobians, use them if possible, warn if they cannot be used + if Js is not None: + if misc.verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): + return Js[self.name] + + # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks + # are an input into the block), then return an empty dict + if not relevant_shocks: + return JacobianDict({}) + else: + invertedJ = {shock_name: {} for shock_name in relevant_shocks} + + # Loop over all inputs/shocks which we want to differentiate with respect to + for shock in relevant_shocks: + invertedJ[shock] = compute_single_shock_curlyJ(self.f, ss, shock) + + # Because we computed the Jacobian of all outputs with respect to each shock (invertedJ[i][o]), + # we need to loop back through to have J[o][i] to map for a given output `o`, shock `i`, + # the Jacobian curlyJ^{o,i}. + J = {o: {} for o in self.output_list} + for o in self.output_list: + for i in relevant_shocks: + # Keep zeros, so we can inspect supplied Jacobians for completeness + if not invertedJ[i][o] or invertedJ[i][o].iszero: + J[o][i] = ZeroMatrix() + else: + if T is not None: + J[o][i] = invertedJ[i][o].matrix(T) + else: + J[o][i] = invertedJ[i][o] + + return JacobianDict(J, name=self.name) + + +def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): + """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" + input_args = {i: ignore(steady_state_dict[i]) for i in misc.input_list(f)} + input_args[shock_name] = AccumulatedDerivative(f_value=steady_state_dict[shock_name]) + + J = {o: {} for o in misc.output_list(f)} + for o, o_name in zip(misc.make_tuple(f(**input_args)), misc.output_list(f)): + if isinstance(o, AccumulatedDerivative): + J[o_name] = SimpleSparse(o.elements) + + return J + + +def make_impulse_uniform_length(out, output_list): + # If the function has multiple outputs + if isinstance(out, tuple): + # Because we know at least one of the outputs in `out` must be of length T + T = np.max([np.size(o) for o in out]) + out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else + misc.numeric_primitive(o) for o in out] + return dict(zip(output_list, misc.make_tuple(out_unif_dim))) + else: + return dict(zip(output_list, misc.make_tuple(misc.numeric_primitive(out)))) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py new file mode 100644 index 0000000..3a2caa7 --- /dev/null +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -0,0 +1,126 @@ +import warnings + +from ..primitives import Block +from ..blocks.simple_block import simple +from ..utilities import graph + + +def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): + """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: + - as @solved(unknowns=..., targets=...) decorator on a single SimpleBlock + - as function solved(blocklist=..., unknowns=..., targets=...) where blocklist + can be any list of blocks + """ + if block_list: + if not name: + name = f"{block_list[0].name}_to_{block_list[-1].name}_solved" + # ordinary call, not as decorator + return SolvedBlock(block_list, name, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + else: + # call as decorator, return function of function + def singleton_solved_block(f): + return SolvedBlock([simple(f)], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + return singleton_solved_block + + +class SolvedBlock(Block): + """SolvedBlocks are mini SHADE models embedded as blocks inside larger SHADE models. + + When creating them, we need to provide the basic ingredients of a SHADE model: the list of + blocks comprising the model, the list on unknowns, and the list of targets. + + When we use .jac to ask for the Jacobian of a SolvedBlock, we are really solving for the 'G' + matrices of the mini SHADE models, which then become the 'curlyJ' Jacobians of the block. + + Similarly, when we use .td to evaluate a SolvedBlock on a path, we are really solving for the + nonlinear transition path such that all internal targets of the mini SHADE model are zero. + """ + + def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): + # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. + self._blocks_unsorted = blocks + + # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks + # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort + # Hence, we will cache that info upon first invocation of the steady_state + self._sorted_indices_w_o_helpers = graph.block_sort(blocks) + self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated + self._required = graph.find_outputs_that_are_intermediate_inputs(blocks) + + # User-facing attributes for accessing blocks + # .blocks_w_helpers meant to only interface with steady_state functionality + # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) + self.blocks_w_helpers = None + self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] + + self.name = name + self.unknowns = unknowns + self.targets = targets + self.solver = solver + self.solver_kwargs = solver_kwargs + + # need to have inputs and outputs!!! + self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) + self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs + + def __repr__(self): + return f"" + + # TODO: Deprecated methods, to be removed! + def ss(self, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(kwargs) + + def td(self, ss, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(ss, **kwargs) + + def jac(self, ss, T=None, shock_list=None, **kwargs): + if shock_list is None: + shock_list = [] + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, shock_list, T, **kwargs) + + def steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, + consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): + # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices + # accounting for HelperBlocks + if self._sorted_indices_w_helpers is None: + self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, helper_blocks=helper_blocks, + calibration=calibration) + self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] + + # Allow override of unknowns/solver, if one wants to evaluate the SolvedBlock at a particular set of + # unknown values akin to the steady_state method of Block + if unknowns is None: + unknowns = self.unknowns + if solver is None: + solver = self.solver + + return super().solve_steady_state(calibration, unknowns, self.targets, solver=solver, + consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) + + def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): + return super().solve_impulse_nonlinear(ss, exogenous=exogenous, + unknowns=list(self.unknowns.keys()), Js=Js, + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) + + def impulse_linear(self, ss, exogenous, T=None, Js=None): + return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + T=T, Js=Js) + + def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): + if exogenous is None: + exogenous = list(self.inputs) + if outputs is None: + outputs = list(self.outputs) + relevant_shocks = [i for i in self.inputs if i in exogenous] + + return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + T=T, outputs=outputs, Js=Js) diff --git a/src/sequence_jacobian/blocks/support/__init__.py b/src/sequence_jacobian/blocks/support/__init__.py new file mode 100644 index 0000000..1e46635 --- /dev/null +++ b/src/sequence_jacobian/blocks/support/__init__.py @@ -0,0 +1,2 @@ +"""Other classes and helpers to aid standard block functionality: .steady_state, .impulse_linear, .impulse_nonlinear, +.jacobian""" diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py new file mode 100644 index 0000000..0d29362 --- /dev/null +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -0,0 +1,69 @@ +"""ImpulseDict class for manipulating impulse responses.""" + +import numpy as np + +from ...steady_state.classes import SteadyStateDict + + +class ImpulseDict: + def __init__(self, impulse): + if isinstance(impulse, ImpulseDict): + self.impulse = impulse.impulse + else: + if not isinstance(impulse, dict): + raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses.') + self.impulse = impulse + + def __repr__(self): + return f'' + + def __iter__(self): + return iter(self.impulse.items()) + + def __or__(self, other): + if not isinstance(other, ImpulseDict): + raise ValueError('Trying to merge an ImpulseDict with something else.') + # Union returns a new ImpulseDict + merged = type(self)(self.impulse) + merged.impulse.update(other.impulse) + return merged + + def __getitem__(self, item): + # Behavior similar to pandas + if isinstance(item, str): + # Case 1: ImpulseDict['C'] returns array + return self.impulse[item] + if isinstance(item, list): + # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts + return type(self)({k: self.impulse[k] for k in item}) + + def __add__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v + other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v + other[k] for k, v in self.impulse.items()}) + + def __sub__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v - other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v - other[k] for k, v in self.impulse.items()}) + + def __mul__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v * other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + + def __rmul__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v * other for k, v in self.impulse.items()}) + if isinstance(other, SteadyStateDict): + return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + + def __truediv__(self, other): + if isinstance(other, (float, int)): + return type(self)({k: v / other for k, v in self.impulse.items()}) + # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero + if isinstance(other, SteadyStateDict): + return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) diff --git a/src/sequence_jacobian/blocks/support/simple_displacement.py b/src/sequence_jacobian/blocks/support/simple_displacement.py new file mode 100644 index 0000000..87ee155 --- /dev/null +++ b/src/sequence_jacobian/blocks/support/simple_displacement.py @@ -0,0 +1,702 @@ +"""Displacement handler classes used by SimpleBlock for .ss, .td, and .jac evaluation to have Dynare-like syntax""" + +import numpy as np +import numbers +from warnings import warn + +from ...utilities.misc import numeric_primitive + +def ignore(x): + if isinstance(x, int): + return IgnoreInt(x) + elif isinstance(x, numbers.Real) and not isinstance(x, int): + return IgnoreFloat(x) + elif isinstance(x, np.ndarray): + return IgnoreVector(x) + else: + raise TypeError(f"{type(x)} is not supported. Must provide either a float or an nd.array as an argument") + + +class IgnoreInt(int): + """This class ignores time displacements of a scalar. + Standard arithmetic operators including +, -, x, /, ** all overloaded to "promote" the result of + any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore + """ + + def __repr__(self): + return f'IgnoreInt({numeric_primitive(self)})' + + @property + def ss(self): + return self + + def __call__(self, index): + return self + + def apply(self, f, **kwargs): + return ignore(f(numeric_primitive(self), **kwargs)) + + def __pos__(self): + return self + + def __neg__(self): + return ignore(-numeric_primitive(self)) + + def __add__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__radd__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) + other) + + def __radd__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__add__(numeric_primitive(self)) + else: + return ignore(other + numeric_primitive(self)) + + def __sub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rsub__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) - other) + + def __rsub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__sub__(numeric_primitive(self)) + else: + return ignore(other - numeric_primitive(self)) + + def __mul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rmul__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) * other) + + def __rmul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__mul__(numeric_primitive(self)) + else: + return ignore(other * numeric_primitive(self)) + + def __truediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rtruediv__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) / other) + + def __rtruediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__truediv__(numeric_primitive(self)) + else: + return ignore(other / numeric_primitive(self)) + + def __pow__(self, power, modulo=None): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + return power.__rpow__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) ** power) + + def __rpow__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__pow__(numeric_primitive(self)) + else: + return ignore(other ** numeric_primitive(self)) + + +class IgnoreFloat(float): + """This class ignores time displacements of a scalar. + Standard arithmetic operators including +, -, x, /, ** all overloaded to "promote" the result of + any arithmetic operation with an Ignore type to an Ignore type. e.g. type(Ignore(1) + 1) is Ignore + """ + + def __repr__(self): + return f'IgnoreFloat({numeric_primitive(self)})' + + @property + def ss(self): + return self + + def __call__(self, index): + return self + + def apply(self, f, **kwargs): + return ignore(f(numeric_primitive(self), **kwargs)) + + def __pos__(self): + return self + + def __neg__(self): + return ignore(-numeric_primitive(self)) + + def __add__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__radd__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) + other) + + def __radd__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__add__(numeric_primitive(self)) + else: + return ignore(other + numeric_primitive(self)) + + def __sub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rsub__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) - other) + + def __rsub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__sub__(numeric_primitive(self)) + else: + return ignore(other - numeric_primitive(self)) + + def __mul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rmul__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) * other) + + def __rmul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__mul__(numeric_primitive(self)) + else: + return ignore(other * numeric_primitive(self)) + + def __truediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rtruediv__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) / other) + + def __rtruediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__truediv__(numeric_primitive(self)) + else: + return ignore(other / numeric_primitive(self)) + + def __pow__(self, power, modulo=None): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + return power.__rpow__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) ** power) + + def __rpow__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__pow__(numeric_primitive(self)) + else: + return ignore(other ** numeric_primitive(self)) + + +class IgnoreVector(np.ndarray): + """This class ignores time displacements of a np.ndarray. + See NumPy documentation on "Subclassing ndarray" for more details on the use of __new__ + for this implementation.""" + + def __new__(cls, x): + obj = np.asarray(x).view(cls) + return obj + + def __repr__(self): + return f'IgnoreVector({numeric_primitive(self)})' + + @property + def ss(self): + return self + + def __call__(self, index): + return self + + def apply(self, f, **kwargs): + return ignore(f(numeric_primitive(self), **kwargs)) + + def __add__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__radd__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) + other) + + def __radd__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__add__(numeric_primitive(self)) + else: + return ignore(other + numeric_primitive(self)) + + def __sub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rsub__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) - other) + + def __rsub__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__sub__(numeric_primitive(self)) + else: + return ignore(other - numeric_primitive(self)) + + def __mul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rmul__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) * other) + + def __rmul__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__mul__(numeric_primitive(self)) + else: + return ignore(other * numeric_primitive(self)) + + def __truediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__rtruediv__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) / other) + + def __rtruediv__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__truediv__(numeric_primitive(self)) + else: + return ignore(other / numeric_primitive(self)) + + def __pow__(self, power, modulo=None): + if isinstance(power, Displace) or isinstance(power, AccumulatedDerivative): + return power.__rpow__(numeric_primitive(self)) + else: + return ignore(numeric_primitive(self) ** power) + + def __rpow__(self, other): + if isinstance(other, Displace) or isinstance(other, AccumulatedDerivative): + return other.__pow__(numeric_primitive(self)) + else: + return ignore(other ** numeric_primitive(self)) + + +class Displace(np.ndarray): + """This class makes time displacements of a time path, given the steady-state value. + Needed for SimpleBlock.td()""" + + def __new__(cls, x, ss=None, name='UNKNOWN'): + obj = np.asarray(x).view(cls) + obj.ss = ss + obj.name = name + return obj + + def __repr__(self): + return f'Displace({numeric_primitive(self)})' + + # TODO: Implemented a very preliminary generalization of Displace to higher-dimensional (>1) ndarrays + # however the rigorous operator overloading/testing has not been checked for higher dimensions. + # Should also implement some checks for the dimension of .ss, to ensure that it's always N-1 + # where we also assume that the *last* dimension is the time dimension + def __call__(self, index): + if index != 0: + if self.ss is None: + raise KeyError(f'Trying to call {self.name}({index}), but steady-state {self.name} not given!') + newx = np.zeros(np.shape(self)) + if index > 0: + newx[..., :-index] = numeric_primitive(self)[..., index:] + newx[..., -index:] = self.ss + else: + newx[..., -index:] = numeric_primitive(self)[..., :index] + newx[..., :-index] = self.ss + return Displace(newx, ss=self.ss) + else: + return self + + def apply(self, f, **kwargs): + return Displace(f(numeric_primitive(self), **kwargs), ss=f(self.ss)) + + def __pos__(self): + return self + + def __neg__(self): + return Displace(-numeric_primitive(self), ss=-self.ss) + + def __add__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(self) + numeric_primitive(other), + ss=self.ss + other.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(self) + numeric_primitive(other), + ss=self.ss + numeric_primitive(other)) + else: + # TODO: See if there is a different, systematic way we want to handle this case. + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(self) + numeric_primitive(other), + ss=self.ss) + + def __radd__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(other) + numeric_primitive(self), + ss=other.ss + self.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(other) + numeric_primitive(self), + ss=numeric_primitive(other) + self.ss) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(other) + numeric_primitive(self), + ss=self.ss) + + def __sub__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(self) - numeric_primitive(other), + ss=self.ss - other.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(self) - numeric_primitive(other), + ss=self.ss - numeric_primitive(other)) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(self) - numeric_primitive(other), + ss=self.ss) + + def __rsub__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(other) - numeric_primitive(self), + ss=other.ss - self.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(other) - numeric_primitive(self), + ss=numeric_primitive(other) - self.ss) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(other) - numeric_primitive(self), + ss=self.ss) + + def __mul__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(self) * numeric_primitive(other), + ss=self.ss * other.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(self) * numeric_primitive(other), + ss=self.ss * numeric_primitive(other)) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(self) * numeric_primitive(other), + ss=self.ss) + + def __rmul__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(other) * numeric_primitive(self), + ss=other.ss * self.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(other) * numeric_primitive(self), + ss=numeric_primitive(other) * self.ss) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(other) * numeric_primitive(self), + ss=self.ss) + + def __truediv__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(self) / numeric_primitive(other), + ss=self.ss / other.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(self) / numeric_primitive(other), + ss=self.ss / numeric_primitive(other)) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(self) / numeric_primitive(other), + ss=self.ss) + + def __rtruediv__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(other) / numeric_primitive(self), + ss=other.ss / self.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(other) / numeric_primitive(self), + ss=numeric_primitive(other) / self.ss) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(other) / numeric_primitive(self), + ss=self.ss) + + def __pow__(self, power): + if isinstance(power, Displace): + return Displace(numeric_primitive(self) ** numeric_primitive(power), + ss=self.ss ** power.ss) + elif np.isscalar(power): + return Displace(numeric_primitive(self) ** numeric_primitive(power), + ss=self.ss ** numeric_primitive(power)) + else: + warn("\n" + f"Applying operation to {power}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(self) ** numeric_primitive(power), + ss=self.ss) + + def __rpow__(self, other): + if isinstance(other, Displace): + return Displace(numeric_primitive(other) ** numeric_primitive(self), + ss=other.ss ** self.ss) + elif np.isscalar(other): + return Displace(numeric_primitive(other) ** numeric_primitive(self), + ss=numeric_primitive(other) ** self.ss) + else: + warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + + f"The resulting Displace object will retain the steady-state value of the original Displace object.") + return Displace(numeric_primitive(other) ** numeric_primitive(self), + ss=self.ss) + + +class AccumulatedDerivative: + """A container for accumulated derivative information to help calculate the sequence space Jacobian + of the outputs of a SimpleBlock with respect to its inputs. + Uses common (i, m) -> x notation as in SimpleSparse (see its docs for more details) as a sparse representation of + a Jacobian of outputs Y at any time t with respect to inputs X at any time s. + + Attributes: + `.elements`: `dict` + A mapping from tuples, (i, m), to floats, x, where i is the index of the non-zero diagonal + relative to the main diagonal (0), where m is the number of initial entries missing from the diagonal + (same conceptually as in SimpleSparse), and x is the value of the accumulated derivatives. + `.f_value`: `float` + The function value of the AccumulatedDerivative to be used when applying the chain rule in finding a subsequent + simple derivative. We can think of a SimpleBlock is a composition of simple functions + (either time displacements, arithmetic operators, etc.), i.e. f_i(f_{i-1}(...f_2(f_1(y))...)), where + at each step i as we are accumulating the derivatives through each simple function, if the derivative of any + f_i requires the chain rule, we will need the function value of the previous f_{i-1} to calculate that derivative. + `._keys`: `list` + The keys from the `.elements` attribute for convenience. + `._fp_values`: `list` + The values from the `.elements` attribute for convenience. `_fp_values` stands for f prime values, i.e. the actual + values of the accumulated derivative themselves. + """ + + def __init__(self, elements={(0, 0): 1.}, f_value=1.): + self.elements = elements + self.f_value = f_value + self._keys = list(self.elements.keys()) + self._fp_values = np.fromiter(self.elements.values(), dtype=float) + + @property + def ss(self): + return ignore(self.f_value) + + def __repr__(self): + formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' + return f'AccumulatedDerivative({formatted})' + + # TODO: Rewrite this comment for clarity once confirmed that the paper's notation will change + # (i, m)/(j, n) correspond to the Q_(-i, m), Q_(-j, n) operators defined for + # Proposition 2 of the Sequence Space Jacobian paper. + # The flipped sign in the code is so that the index 'i' matches the k(i) notation + # for writing SimpleBlock functions. Thus, it follows the same convention as SimpleSparse. + # Also because __call__ on a AccumulatedDerivative is a simple shift operator, it will take the form + # Q_(-i, 0) being applied to Q_(-j, n) (following the notation in the paper) + # s.t. Q_(-i, 0) Q_(-j, n) = Q(k,l) + def __call__(self, i): + keys = [(i + j, compute_l(-i, 0, -j, n)) for j, n in self._keys] + return AccumulatedDerivative(elements=dict(zip(keys, self._fp_values)), f_value=self.f_value) + + def apply(self, f, h=1e-5, **kwargs): + if f == np.log: + return AccumulatedDerivative(elements=dict(zip(self._keys, + [1 / self.f_value * x for x in self._fp_values])), + f_value=np.log(self.f_value)) + else: + return AccumulatedDerivative(elements=dict(zip(self._keys, [(f(self.f_value + h, **kwargs) - + f(self.f_value - h, **kwargs)) / (2 * h) * x + for x in self._fp_values])), + f_value=f(self.f_value, **kwargs)) + + def __pos__(self): + return AccumulatedDerivative(elements=dict(zip(self._keys, +self._fp_values)), f_value=+self.f_value) + + def __neg__(self): + return AccumulatedDerivative(elements=dict(zip(self._keys, -self._fp_values)), f_value=-self.f_value) + + def __add__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), + f_value=self.f_value + numeric_primitive(other)) + elif isinstance(other, AccumulatedDerivative): + elements = self.elements.copy() + for im, x in other.elements.items(): + if im in elements: + elements[im] += x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = x + + return AccumulatedDerivative(elements=elements, f_value=self.f_value + other.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __radd__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), + f_value=numeric_primitive(other) + self.f_value) + elif isinstance(other, AccumulatedDerivative): + elements = other.elements.copy() + for im, x in self.elements.items(): + if im in elements: + elements[im] += x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = x + + return AccumulatedDerivative(elements=elements, f_value=other.f_value + self.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __sub__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values)), + f_value=self.f_value - numeric_primitive(other)) + elif isinstance(other, AccumulatedDerivative): + elements = self.elements.copy() + for im, x in other.elements.items(): + if im in elements: + elements[im] -= x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = -x + + return AccumulatedDerivative(elements=elements, f_value=self.f_value - other.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __rsub__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, -self._fp_values)), + f_value=numeric_primitive(other) - self.f_value) + elif isinstance(other, AccumulatedDerivative): + elements = other.elements.copy() + for im, x in self.elements.items(): + if im in elements: + elements[im] -= x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = -x + + return AccumulatedDerivative(elements=elements, f_value=other.f_value - self.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __mul__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values * numeric_primitive(other))), + f_value=self.f_value * numeric_primitive(other)) + elif isinstance(other, AccumulatedDerivative): + return AccumulatedDerivative(elements=(self * other.f_value + other * self.f_value).elements, + f_value=self.f_value * other.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __rmul__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, numeric_primitive(other) * self._fp_values)), + f_value=numeric_primitive(other) * self.f_value) + elif isinstance(other, AccumulatedDerivative): + return AccumulatedDerivative(elements=(other * self.f_value + self * other.f_value).elements, + f_value=other.f_value * self.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __truediv__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, self._fp_values / numeric_primitive(other))), + f_value=self.f_value / numeric_primitive(other)) + elif isinstance(other, AccumulatedDerivative): + return AccumulatedDerivative(elements=((other.f_value * self - self.f_value * other) / + (other.f_value ** 2)).elements, + f_value=self.f_value / other.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __rtruediv__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, -numeric_primitive(other) / + self.f_value ** 2 * self._fp_values)), + f_value=numeric_primitive(other) / self.f_value) + elif isinstance(other, AccumulatedDerivative): + return AccumulatedDerivative(elements=((self.f_value * other - other.f_value * self) / + (self.f_value ** 2)).elements, f_value=other.f_value / self.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __pow__(self, power, modulo=None): + if np.isscalar(power): + return AccumulatedDerivative(elements=dict(zip(self._keys, numeric_primitive(power) * self.f_value + ** numeric_primitive(power - 1) * self._fp_values)), + f_value=self.f_value ** numeric_primitive(power)) + elif isinstance(power, AccumulatedDerivative): + return AccumulatedDerivative(elements=(self.f_value ** (power.f_value - 1) * ( + power.f_value * self + power * self.f_value * np.log(self.f_value))).elements, + f_value=self.f_value ** power.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + def __rpow__(self, other): + if np.isscalar(other): + return AccumulatedDerivative(elements=dict(zip(self._keys, np.log(other) * numeric_primitive(other) ** + self.f_value * self._fp_values)), + f_value=numeric_primitive(other) ** self.f_value) + elif isinstance(other, AccumulatedDerivative): + return AccumulatedDerivative(elements=(other.f_value ** (self.f_value - 1) * ( + self.f_value * other + self * other.f_value * np.log(other.f_value))).elements, + f_value=other.f_value ** self.f_value) + else: + raise NotImplementedError("This operation is not yet supported for non-scalar arguments") + + +def compute_l(i, m, j, n): + """Computes the `l` index from the composition of shift operators, Q_{i, m} Q_{j, n} = Q_{k, l} in Proposition 2 + of the paper (regarding efficient multiplication of simple Jacobians).""" + if i >= 0 and j >= 0: + return max(m - j, n) + elif i >= 0 and j <= 0: + return max(m, n) + min(i, -j) + elif i <= 0 and j >= 0 and i + j >= 0: + return max(m - i - j, n) + elif i <= 0 and j >= 0 and i + j <= 0: + return max(n + i + j, m) + else: + return max(m, n + i) + + +# TODO: This needs its own unit test +def vectorize_func_over_time(func, *args): + """In `args` some arguments will be Displace objects and others will be Ignore/IgnoreVector objects. + The Displace objects will have an extra time dimension (as its first dimension). + We need to ensure that `func` is evaluated at the non-time dependent steady-state value of + the Ignore/IgnoreVectors and at each of the time-dependent values, t, of the Displace objects or in other + words along its time path. + """ + d_inds = [i for i in range(len(args)) if isinstance(args[i], Displace)] + x_path = [] + + # np.shape(args[d_inds[0]])[0] is T, the size of the first dimension of the first Displace object + # provided in args (assume all Displaces are the same shape s.t. they're conformable) + for t in range(np.shape(args[d_inds[0]])[0]): + x_path.append(func(*[args[i][t] if i in d_inds else args[i] for i in range(len(args))])) + + return np.array(x_path) + + +def apply_function(func, *args, **kwargs): + """Ensure that for generic functions called within a block and acting on a Displace object + properly instantiates the steady state value of the created Displace object""" + if np.any([isinstance(x, Displace) for x in args]): + x_path = vectorize_func_over_time(func, *args) + return Displace(x_path, ss=func(*[x.ss if isinstance(x, Displace) else numeric_primitive(x) for x in args])) + elif np.any([isinstance(x, AccumulatedDerivative) for x in args]): + raise NotImplementedError( + "Have not yet implemented general apply_function functionality for AccumulatedDerivatives") + else: + return func(*args, **kwargs) diff --git a/src/sequence_jacobian/devtools/__init__.py b/src/sequence_jacobian/devtools/__init__.py new file mode 100644 index 0000000..9795de6 --- /dev/null +++ b/src/sequence_jacobian/devtools/__init__.py @@ -0,0 +1,3 @@ +"""Tools for debugging, developing, and deprecating code""" + +from . import analysis, debug, deprecate, upgrade diff --git a/src/sequence_jacobian/devtools/analysis.py b/src/sequence_jacobian/devtools/analysis.py new file mode 100644 index 0000000..77e90d8 --- /dev/null +++ b/src/sequence_jacobian/devtools/analysis.py @@ -0,0 +1,242 @@ +"""Low-level tools/classes for analyzing sequence-jacobian model DAGs to support debugging""" + +import numpy as np +import xarray as xr +from collections.abc import Iterable + +from ..utilities import graph + + +class BlockIONetwork: + """ + A 3-d axis-labeled DataArray (blocks x inputs x outputs), which allows for the analysis of the input-output + structure of a DAG. + """ + def __init__(self, blocks, calibration=None, ignore_helpers=True): + topsorted, inset, outset = graph.block_sort(blocks, return_io=True, calibration=calibration, + ignore_helpers=ignore_helpers) + self.blocks = {b.name: b for b in blocks} + self.blocks_names = list(self.blocks.keys()) + self.blocks_as_list = list(self.blocks.values()) + self.var_names = list(inset.union(outset)) + self.darray = xr.DataArray(np.zeros((len(blocks), len(self.var_names), len(self.var_names))), + coords=[self.blocks_names, self.var_names, self.var_names], + dims=["blocks", "inputs", "outputs"]) + + def __repr__(self): + return self.darray.__repr__() + + # User-facing "print" methods + def print_block_links(self, block_name): + print(f" Links in {block_name}") + print(" " + "-" * (len(f" Links in {block_name}"))) + + link_inds = np.nonzero(self._subset_by_block(block_name)).data + links = [] + for i in range(np.shape(link_inds)[0]): + i_ind, o_ind = link_inds[:, i] + i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) + o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) + links.append([i_var, o_var]) + self._print_links(links) + + def print_all_var_links(self, var_name, calibration=None, ignore_helpers=True): + print(f" Links from {var_name}") + print(" " + "-" * (len(f" Links for {var_name}"))) + + links = self.find_all_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) + self._print_links(links) + + @staticmethod + def _print_links(links): + link_strs = [] + # Create " -> " linked strings and sort them for nicer printing + for link in links: + link_strs.append(" " + " -> ".join(link)) + link_strs.sort() + for link_str in link_strs: + print(link_str) + print("") # To break lines + + def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): + print(f" Links between {unknowns} and {targets}") + print(" " + "-" * (len(f" Links between {unknowns} and {targets}"))) + unknown_target_net = self.find_unknowns_targets_links(unknowns, targets, calibration=calibration, + ignore_helpers=ignore_helpers) + print(unknown_target_net) + print("") # To break lines + + # TODO: Implement an enhancement to display the "closest" link if missing. + def query_var_link(self, input_var, output_var, calibration=None, ignore_helpers=True): + all_links = self.find_all_var_links(input_var, calibration=calibration, ignore_helpers=ignore_helpers) + link_paths = [] + for link in all_links: + if link[0] == input_var and link[-1] == output_var: + link_paths.append(link) + if link_paths: + print(f" Links between {input_var} and {output_var}") + print(" " + "-" * (len(f" Links between {input_var} and {output_var}"))) + self._print_links(link_paths) + else: + print(f"There are no links within the DAG connecting {input_var} to {output_var}") + + # User-facing "analysis" methods + def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, + calibration=None, ignore_helpers=True): + """ + Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG + + Parameters + ---------- + inputs_to_be_recorded: `list(str)` + A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork + block_input_args: `dict` + A dict of variable/parameter names and values (typically from the steady state of the model) on which, + a block can perform a valid evaluation + calibration: `dict` or None + Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies + ignore_helpers: bool + Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies + """ + block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, ignore_helpers=ignore_helpers) + for input_var in inputs_to_be_recorded: + all_input_vars = set(input_var) + for ib in block_inds_sorted: + ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} + # This extra step is needed because some arguments required for calling .jac on + # HetBlock and SolvedBlock are not a part of .inputs + ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) + io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) + if io_links: + self._record_io_links(self.blocks_names[ib], io_links) + # Need to also track the paths of outputs which could be intermediate inputs further down the DAG + all_input_vars = all_input_vars.union(set(io_links.keys())) + + def find_all_var_links(self, var_name, calibration=None, ignore_helpers=True): + # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those + # `outputs` and instantiate the initial list of links + link_inds = np.nonzero(self._subset_by_vars(var_name).data) + links = [[var_name] for i in range(len(link_inds[0]))] + + block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, + ignore_helpers=ignore_helpers) + required = graph.find_outputs_that_are_intermediate_inputs(self.blocks_as_list, ignore_helpers=ignore_helpers) + intermediates = set() + for ib in block_inds_sorted: + # Note: This block is ordered before the bottom block of code since the intermediate outputs from a block + # `ib` do not need to be checked as inputs to that same block `ib`, only the subsequent blocks + if intermediates: + intm_link_inds = np.nonzero(self._subset_by_vars(list(intermediates)).data) + # If there are `intermediate` inputs that have been recorded, we need to find the *indirect* links + for iil, iilb in enumerate(intm_link_inds[0]): + # Check if those inputs are inputs to block `ib` + if ib == iilb: + # If so, repeat the logic from below, where you find the input-output var link + o_var = str(self._subset_by_vars(list(intermediates)).coords["outputs"][intm_link_inds[2][iil]].data) + intm_i_var = str(self._subset_by_vars(list(intermediates)).coords["inputs"][intm_link_inds[1][iil]].data) + + # And add it to the set of all links, recording this new links' output if it hasn't appeared + # before and if it is an intermediate input + links.append([intm_i_var, o_var]) + if o_var in required: + intermediates = intermediates.union([o_var]) + + # Check if `var_name` enters into that block as an input, and if so add the link between it and the output + # it directly affects, recording that output as an intermediate input if needed + # Note: link_inds' 0-th row indicates the blocks and 1st row indicates the outputs + for il, ilb in enumerate(link_inds[0]): + if ib == ilb: + o_var = str(self._subset_by_vars(var_name).coords["outputs"][link_inds[1][il]].data) + links[il].append(o_var) + if o_var in required: + intermediates = intermediates.union([o_var]) + return _compose_dyad_links(links) + + def find_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): + unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), + coords=[unknowns, targets], + dims=["inputs", "outputs"]) + for u in unknowns: + links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) + for link in links: + if link[0] == u and link[-1] in targets: + unknown_target_net.loc[u, link[-1]] = 1. + return unknown_target_net + + # Analysis support methods + def _subset_by_block(self, block_name): + return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] + + def _subset_by_vars(self, vars_names): + if isinstance(vars_names, Iterable): + return self.darray.loc[[b.name for b in self.blocks.values() if np.any(v in b.inputs for v in vars_names)], + vars_names, :] + else: + return self.darray.loc[[b.name for b in self.blocks.values() if vars_names in b.inputs], vars_names, :] + + def _record_io_links(self, block_name, io_links): + for o, i in io_links.items(): + self.darray.loc[block_name, i, o] = 1. + + +def _compose_dyad_links(links): + links_composed = [] + inds_to_ignore = set() + outputs = set() + + for il, link in enumerate(links): + if il in inds_to_ignore: + continue + if links_composed: + if link[0] in outputs: + # Since `link` has as its input one of the outputs recorded from prior links in `links_composed` + # search through the links in `links_composed` to see which links need to be extended with `link` + # and the other links with the same input as `link` + link_extensions = [] + # Potential link extensions will only be located past the stage il that we are at + for il_e in range(il, len(links)): + if links[il_e][0] == link[0]: + link_extensions.append(links[il_e]) + outputs = outputs.union([links[il_e][-1]]) + inds_to_ignore = inds_to_ignore.union([il_e]) + + links_to_add = [] + inds_to_omit = [] + for il_c, link_c in enumerate(links_composed): + if link_c[-1] == link[0]: + inds_to_omit.append(il_c) + links_to_add.extend([link_c + [ext[-1]] for ext in link_extensions]) + + links_composed = [link_c for i, link_c in enumerate(links_composed) if i not in inds_to_omit] + links_to_add + else: + links_composed.append(link) + outputs = outputs.union([link[-1]]) + else: + links_composed.append(link) + outputs = outputs.union([link[-1]]) + return links_composed + + +def find_io_links(block, input_args, block_input_args): + """ + For a given `block`, see which output arguments the input argument `input_args` affects + + Parameters + ---------- + block: `Block` object + One of the various kinds of `Block` objects (`SimpleBlock`, `HetBlock`, etc.) + input_args: `str` or `list(str)` + The input arguments, whose paths through the block to the output variables we want to see + block_input_args: `dict{str: num}` + The rest of the input arguments required to evaluate the block's Jacobian + + Returns + ------- + links: `dict{str: list(str)}` + A dict with *output arguments* as keys, and the input arguments that affect it as values + """ + J = block.jac(ss=block_input_args, T=2, shock_list=input_args) + links = {} + for o in J.outputs: + links[o] = list(J.nesteddict[o].keys()) + return links diff --git a/src/sequence_jacobian/devtools/debug.py b/src/sequence_jacobian/devtools/debug.py new file mode 100644 index 0000000..48e22ec --- /dev/null +++ b/src/sequence_jacobian/devtools/debug.py @@ -0,0 +1,106 @@ +"""Top-level tools to help users debug their sequence-jacobian code""" + +import warnings +import numpy as np + +from . import analysis +from ..utilities import graph + + +def ensure_computability(blocks, calibration=None, unknowns_ss=None, + exogenous=None, unknowns=None, targets=None, ss=None, + verbose=False, fragile=True, ignore_helpers=True): + # Check if `calibration` and `unknowns` jointly have all of the required variables needed to be able + # to calculate a steady state. If ss is provided, assume the user doesn't need to check this + if calibration and unknowns_ss and not ss: + ensure_all_inputs_accounted_for(blocks, calibration, unknowns_ss, verbose=verbose, fragile=fragile) + + # Check if unknowns and exogenous are not outputs of any blocks, and that targets are not inputs to any blocks + if exogenous and unknowns and targets: + ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous + unknowns, targets, + verbose=verbose, fragile=fragile) + + # Check if there are any "broken" links between unknowns and targets, i.e. if there are any unknowns that don't + # affect any targets, or if there are any targets that aren't affected by any unknowns + if unknowns and targets and ss: + ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile) + + +# To ensure that no input argument that is required for one of the blocks to evaluate is missing +def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False, fragile=True): + variables_accounted_for = set(unknowns.keys()).union(set(calibration.keys())) + all_inputs = set().union(*[b.inputs for b in blocks]) + required = graph.find_outputs_that_are_intermediate_inputs(blocks) + non_computed_inputs = all_inputs.difference(required) + + variables_unaccounted_for = non_computed_inputs.difference(variables_accounted_for) + if variables_unaccounted_for: + if fragile: + raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" + f"parameters: {variables_unaccounted_for}") + else: + warnings.warn(f"\nThe following variables were not listed as unknowns or provided as fixed variables/" + f"parameters: {variables_unaccounted_for}") + if verbose: + print("This DAG accounts for all inputs variables.") + + +def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unknowns, targets, + verbose=False, fragile=True): + cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks) + invalid_xu = [] + invalid_targ = [] + for xu in exogenous_unknowns: + if xu not in cand_xu: + invalid_xu.append(xu) + for targ in targets: + if targ not in cand_targets: + invalid_targ.append(targ) + if invalid_xu or invalid_targ: + if fragile: + raise RuntimeError(f"The following exogenous/unknowns are invalid candidates: {invalid_xu}\n" + f"The following targets are invalid candidates: {invalid_targ}") + else: + warnings.warn(f"\nThe following exogenous/unknowns are invalid candidates: {invalid_xu}\n" + f"The following targets are invalid candidates: {invalid_targ}") + if verbose: + print("The provided exogenous/unknowns and targets are all valid candidates for this DAG.") + + +def find_candidate_unknowns_and_targets(block_list, verbose=False): + dep, inputs, outputs = graph.block_sort(block_list, return_io=True) + required = graph.find_outputs_that_are_intermediate_inputs(block_list) + + # Candidate exogenous and unknowns (also includes parameters): inputs that are not outputs of any block + # Candidate targets: outputs that are not inputs to any block + cand_xu = inputs.difference(required) + cand_targets = outputs.difference(required) + + if verbose: + print(f"Candidate exogenous/unknowns: {cand_xu}\n" + f"Candidate targets: {cand_targets}") + + return cand_xu, cand_targets + + +def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True): + io_net = analysis.BlockIONetwork(blocks) + io_net.record_input_variables_paths(unknowns, ss) + ut_net = io_net.find_unknowns_targets_links(unknowns, targets) + broken_unknowns = [] + broken_targets = [] + for u in unknowns: + if not np.any(ut_net.loc[u, :]): + broken_unknowns.append(u) + for t in targets: + if not np.any(ut_net.loc[:, t]): + broken_targets.append(t) + if broken_unknowns or broken_targets: + if fragile: + raise RuntimeError(f"The following unknowns don't affect any targets: {broken_unknowns}\n" + f"The following targets aren't affected by any unknowns: {broken_targets}") + else: + warnings.warn(f"\nThe following unknowns don't affect any targets: {broken_unknowns}\n" + f"The following targets aren't affected by any unknowns: {broken_targets}") + if verbose: + print("This DAG does not contain any broken links between unknowns and targets.") diff --git a/src/sequence_jacobian/devtools/deprecate.py b/src/sequence_jacobian/devtools/deprecate.py new file mode 100644 index 0000000..e7b4cb1 --- /dev/null +++ b/src/sequence_jacobian/devtools/deprecate.py @@ -0,0 +1,22 @@ +"""Tools for deprecating older SSJ code conventions in favor of newer conventions""" + +import warnings + +# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, by temporarily +# providing support for old conventions via deprecated methods, providing time to allow for a seamless upgrade +# to newer versions sequence-jacobian. + +# TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions +# themselves. + + +def rename_output_list_to_outputs(outputs=None, output_list=None): + if output_list is not None: + warnings.warn("The output_list kwarg has been deprecated and replaced with the outputs kwarg.", + DeprecationWarning) + if outputs is None: + return output_list + else: + return list(set(outputs) | set(output_list)) + else: + return outputs diff --git a/src/sequence_jacobian/devtools/upgrade.py b/src/sequence_jacobian/devtools/upgrade.py new file mode 100644 index 0000000..e2994d8 --- /dev/null +++ b/src/sequence_jacobian/devtools/upgrade.py @@ -0,0 +1,39 @@ +"""Tools for upgrading from older SSJ code conventions""" + +# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, and who +# may want additional support/tools for ensuring that their attempts to upgrade to use newer versions of +# sequence-jacobian has been successfully. + +import numpy as np + + +def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): + """ + This code is meant to provide a quick comparison of `ss_ref` the reference steady state dict from old code, and + `ss_comp` the steady state computed from the newer code. + """ + if name_map is None: + name_map = {} + + # Compare the steady state values present in both ss_ref and ss_comp + for key_ref in ss_ref.keys(): + if key_ref in ss_comp.keys(): + key_comp = key_ref + elif key_ref in name_map: + key_comp = name_map[key_ref] + else: + continue + if verbose: + if np.isscalar(ss_ref[key_ref]): + print(f"{key_ref} resid: {abs(ss_ref[key_ref] - ss_comp[key_comp])}") + else: + print(f"{key_ref} resid: {np.linalg.norm(ss_ref[key_ref].flatten() - ss_comp[key_comp].flatten(), np.inf)}") + else: + assert np.isclose(ss_ref[key_ref], ss_comp[key_comp]) + + # Show the steady state values present in only one of ss_ref or ss_comp + ss_ref_incl_mapped = set(ss_ref.keys()) - set(name_map.keys()) + ss_comp_incl_mapped = set(ss_comp.keys()) - set(name_map.values()) + diff_keys = ss_ref_incl_mapped.symmetric_difference(ss_comp_incl_mapped) + if diff_keys: + print(f"The keys present in only one of the two steady state dicts are {diff_keys}") diff --git a/src/sequence_jacobian/estimation.py b/src/sequence_jacobian/estimation.py new file mode 100644 index 0000000..a633640 --- /dev/null +++ b/src/sequence_jacobian/estimation.py @@ -0,0 +1,86 @@ +"""Functions for calculating the log likelihood of a model from its impulse responses""" + +import numpy as np +import scipy.linalg as linalg +from numba import njit + +'''Part 1: compute covariances at all lags and log likelihood''' + + +def all_covariances(M, sigmas): + """Use Fast Fourier Transform to compute covariance function between O vars up to T-1 lags. + + See equation (108) in appendix B.5 of paper for details. + + Parameters + ---------- + M : array (T*O*Z), stacked impulse responses of nO variables to nZ shocks (MA(T-1) representation) + sigmas : array (Z), standard deviations of shocks + + Returns + ---------- + Sigma : array (T*O*O), covariance function between O variables for 0, ..., T-1 lags + """ + T = M.shape[0] + dft = np.fft.rfftn(M, s=(2 * T - 2,), axes=(0,)) + total = (dft.conjugate() * sigmas**2) @ dft.swapaxes(1, 2) + return np.fft.irfftn(total, s=(2 * T - 2,), axes=(0,))[:T] + + +def log_likelihood(Y, Sigma, sigma_measurement=None): + """Given second moments, compute log-likelihood of data Y. + + Parameters + ---------- + Y : array (Tobs*O) + stacked data for O observables over Tobs periods + Sigma : array (T*O*O) + covariance between observables in model for 0, ... , T lags (e.g. from all_covariances) + sigma_measurement : [optional] array (O) + std of measurement error for each observable, assumed zero if not provided + + Returns + ---------- + L : scalar, log-likelihood + """ + Tobs, nO = Y.shape + if sigma_measurement is None: + sigma_measurement = np.zeros(nO) + V = build_full_covariance_matrix(Sigma, sigma_measurement, Tobs) + y = Y.ravel() + return log_likelihood_formula(y, V) + + +'''Part 2: helper functions''' + + +def log_likelihood_formula(y, V): + """Implements multivariate normal log-likelihood formula using Cholesky with data vector y and variance V. + Calculates -log det(V)/2 - y'V^(-1)y/2 + """ + V_factored = linalg.cho_factor(V) + quadratic_form = np.dot(y, linalg.cho_solve(V_factored, y)) + log_determinant = 2*np.sum(np.log(np.diag(V_factored[0]))) + return -(log_determinant + quadratic_form) / 2 + + +@njit +def build_full_covariance_matrix(Sigma, sigma_measurement, Tobs): + """Takes in T*O*O array Sigma with covariances at each lag t, + assembles them into (Tobs*O)*(Tobs*O) matrix of covariances, including measurement errors. + """ + T, O, O = Sigma.shape + V = np.empty((Tobs, O, Tobs, O)) + for t1 in range(Tobs): + for t2 in range(Tobs): + if abs(t1-t2) >= T: + V[t1, :, t2, :] = np.zeros((O, O)) + else: + if t1 < t2: + V[t1, : , t2, :] = Sigma[t2-t1, :, :] + elif t1 > t2: + V[t1, : , t2, :] = Sigma[t1-t2, :, :].T + else: + # want exactly symmetric + V[t1, :, t2, :] = (np.diag(sigma_measurement**2) + (Sigma[0, :, :]+Sigma[0, :, :].T)/2) + return V.reshape((Tobs*O, Tobs*O)) diff --git a/src/sequence_jacobian/jacobian/__init__.py b/src/sequence_jacobian/jacobian/__init__.py new file mode 100644 index 0000000..57070a8 --- /dev/null +++ b/src/sequence_jacobian/jacobian/__init__.py @@ -0,0 +1 @@ +"""Jacobian computation and support functions""" diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py new file mode 100644 index 0000000..b522f7d --- /dev/null +++ b/src/sequence_jacobian/jacobian/classes.py @@ -0,0 +1,471 @@ +"""Various classes to support the computation of Jacobians""" + +from abc import ABCMeta +import copy +import numpy as np + +from . import support + + +class Jacobian(metaclass=ABCMeta): + """An abstract base class encompassing all valid types representing Jacobians, which include + np.ndarray, IdentityMatrix, ZeroMatrix, and SimpleSparse.""" + pass + +# Make np.ndarray a child class of Jacobian +Jacobian.register(np.ndarray) + + +class IdentityMatrix(Jacobian): + """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, + use to initialize Jacobian of a variable wrt itself""" + __array_priority__ = 10_000 + + def sparse(self): + """Equivalent SimpleSparse representation, less efficient operations but more general.""" + return SimpleSparse({(0, 0): 1}) + + def matrix(self, T): + return np.eye(T) + + def __matmul__(self, other): + """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" + return copy.deepcopy(other) + + def __rmatmul__(self, other): + return copy.deepcopy(other) + + def __mul__(self, a): + return a*self.sparse() + + def __rmul__(self, a): + return self.sparse()*a + + def __add__(self, x): + return self.sparse() + x + + def __radd__(self, x): + return x + self.sparse() + + def __sub__(self, x): + return self.sparse() - x + + def __rsub__(self, x): + return x - self.sparse() + + def __neg__(self): + return -self.sparse() + + def __pos__(self): + return self + + def __repr__(self): + return 'IdentityMatrix' + + +class ZeroMatrix(Jacobian): + """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, + use in common case where some outputs don't depend on inputs""" + __array_priority__ = 10_000 + + def sparse(self): + return SimpleSparse({(0, 0): 0}) + + def matrix(self, T): + return np.zeros((T,T)) + + def __matmul__(self, other): + if isinstance(other, np.ndarray) and other.ndim == 1: + return np.zeros_like(other) + else: + return self + + def __rmatmul__(self, other): + return self @ other + + def __mul__(self, a): + return self + + def __rmul__(self, a): + return self + + # copies seem inefficient here, try to live without them + def __add__(self, x): + return x + + def __radd__(self, x): + return x + + def __sub__(self, x): + return -x + + def __rsub__(self, x): + return x + + def __neg__(self): + return self + + def __pos__(self): + return self + + def __repr__(self): + return 'ZeroMatrix' + + +class SimpleSparse(Jacobian): + """Efficient representation of sparse linear operators, which are linear combinations of basis + operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s + (measured by # above main diagonal) and m is number of initial entries missing. + + Examples of such basis operators: + - (0, 0) is identity operator + - (0, 2) is identity operator with first two '1's on main diagonal missing + - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator + - (-1, 1) has 1s on diagonal below main diagonal, except first column + + The linear combination of these basis operators that makes up a given SimpleSparse object is + stored as a dict 'elements' mapping (i, m) -> x. + + The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need + the more general basis (i, m) to ensure closure under multiplication. + + These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space + Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation + for writing SimpleBlock functions. + + The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix + operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less + interchangeably with ordinary NumPy matrices. + """ + + # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules + __array_priority__ = 1000 + + def __init__(self, elements): + self.elements = elements + self.indices, self.xs = None, None + + @staticmethod + def from_simple_diagonals(elements): + """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" + return SimpleSparse({(i, 0): x for i, x in elements.items()}) + + def matrix(self, T): + """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" + return self + np.zeros((T, T)) + + def array(self): + """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) + and one size-N array of floats with entries x. + + This is needed for Numba to take as input. Cache for efficiency. + """ + if self.indices is not None: + return self.indices, self.xs + else: + indices, xs = zip(*self.elements.items()) + self.indices, self.xs = np.array(indices), np.array(xs) + return self.indices, self.xs + + @property + def T(self): + """Transpose""" + return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) + + @property + def iszero(self): + return not self.nonzero().elements + + def nonzero(self): + elements = self.elements.copy() + for im, x in self.elements.items(): + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + return SimpleSparse(elements) + + def __pos__(self): + return self + + def __neg__(self): + return SimpleSparse({im: -x for im, x in self.elements.items()}) + + def __matmul__(self, A): + if isinstance(A, SimpleSparse): + # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs + return SimpleSparse(support.multiply_rs_rs(self, A)) + elif isinstance(A, np.ndarray): + # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing + indices, xs = self.array() + if A.ndim == 2: + return support.multiply_rs_matrix(indices, xs, A) + elif A.ndim == 1: + return support.multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] + else: + return NotImplemented + else: + return NotImplemented + + def __rmatmul__(self, A): + # multiplication rule when this object is on right (will only be called when left is matrix) + # for simplicity, just use transpose to reduce this to previous cases + return (self.T @ A.T).T + + def __add__(self, A): + if isinstance(A, SimpleSparse): + # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap + elements = self.elements.copy() + for im, x in A.elements.items(): + if im in elements: + elements[im] += x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = x + return SimpleSparse(elements) + else: + # add SimpleSparse to T*T matrix + if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: + return NotImplemented + T = A.shape[0] + + # fancy trick to do this efficiently by writing A as flat vector + # then (i, m) can be mapped directly to NumPy slicing! + A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy + for (i, m), x in self.elements.items(): + if i < 0: + A[T * (-i) + (T + 1) * m::T + 1] += x + else: + A[i + (T + 1) * m:(T - i) * T:T + 1] += x + return A.reshape((T, T)) + + def __radd__(self, A): + try: + return self + A + except: + print(self) + print(A) + raise + + def __sub__(self, A): + # slightly inefficient implementation with temporary for simplicity + return self + (-A) + + def __rsub__(self, A): + return -self + A + + def __mul__(self, a): + if not np.isscalar(a): + return NotImplemented + return SimpleSparse({im: a * x for im, x in self.elements.items()}) + + def __rmul__(self, a): + return self * a + + def __repr__(self): + formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' + return f'SimpleSparse({formatted})' + + def __eq__(self, s): + return self.elements == s.elements + + +class NestedDict: + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + if isinstance(nesteddict, NestedDict): + self.nesteddict = nesteddict.nesteddict + self.outputs = nesteddict.outputs + self.inputs = nesteddict.inputs + self.name = nesteddict.name + else: + self.nesteddict = nesteddict + if outputs is None: + outputs = list(nesteddict.keys()) + if inputs is None: + inputs = [] + for v in nesteddict.values(): + inputs.extend(list(v)) + inputs = deduplicate(inputs) + + self.outputs = list(outputs) + self.inputs = list(inputs) + if name is None: + # TODO: Figure out better default naming scheme for NestedDicts + self.name = "NestedDict" + else: + self.name = name + + def __repr__(self): + return f'<{type(self).__name__} outputs={self.outputs}, inputs={self.inputs}>' + + def __iter__(self): + return iter(self.outputs) + + def __or__(self, other): + # non-in-place merge: make a copy, then update + merged = type(self)(self.nesteddict, self.outputs, self.inputs) + merged.update(other) + return merged + + def __getitem__(self, x): + if isinstance(x, str): + # case 1: just a single output, give subdict + return self.nesteddict[x] + elif isinstance(x, tuple): + # case 2: tuple, referring to output and input + o, i = x + o = self.outputs if o == slice(None, None, None) else o + i = self.inputs if i == slice(None, None, None) else i + if isinstance(o, str): + if isinstance(i, str): + # case 2a: one output, one input, return single Jacobian + return self.nesteddict[o][i] + else: + # case 2b: one output, multiple inputs, return dict + return {ii: self.nesteddict[o][ii] for ii in i} + else: + # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i + i = (i,) if isinstance(i, str) else i + return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) + elif isinstance(x, list) or isinstance(x, set): + # case 3: assume that list or set refers just to outputs, get all of those + return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) + else: + raise ValueError(f'Tried to get impermissible item {x}') + + def get(self, *args, **kwargs): + # this is for compatibility, not a huge fan + return self.nesteddict.get(*args, **kwargs) + + def update(self, J): + if set(self.inputs) != set(J.inputs): + raise ValueError \ + (f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') + if not set(self.outputs).isdisjoint(J.outputs): + raise ValueError \ + (f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') + self.outputs = self.outputs + J.outputs + self.nesteddict = {**self.nesteddict, **J.nesteddict} + + # Ensure that every output in self has either a Jacobian or filler value for each input, + # s.t. all inputs map to all outputs + def complete(self, filler): + nesteddict = {} + for o in self.outputs: + nesteddict[o] = dict(self.nesteddict[o]) + for i in self.inputs: + if i not in nesteddict[o]: + nesteddict[o][i] = filler + return type(self)(nesteddict, self.outputs, self.inputs) + + +def deduplicate(mylist): + """Remove duplicates while otherwise maintaining order""" + return list(dict.fromkeys(mylist)) + + +class JacobianDict(NestedDict): + def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + ensure_valid_jacobiandict(nesteddict) + super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) + + @staticmethod + def identity(ks): + return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() + + def complete(self): + return super().complete(ZeroMatrix()) + + def addinputs(self): + """Add any inputs that were not already in output list as outputs, with the identity""" + inputs = [x for x in self.inputs if x not in self.outputs] + return self | JacobianDict.identity(inputs) + + def __matmul__(self, x): + if isinstance(x, JacobianDict): + return self.compose(x) + else: + return self.apply(x) + + def __bool__(self): + return bool(self.outputs) and bool(self.inputs) + + def compose(self, J): + o_list = self.outputs + m_list = tuple(set(self.inputs) & set(J.outputs)) + i_list = J.inputs + + J_om = self.complete().nesteddict + J_mi = J.complete().nesteddict + J_oi = {} + + for o in o_list: + J_oi[o] = {} + for i in i_list: + Jout = ZeroMatrix() + for m in m_list: + J_om[o][m] + J_mi[m][i] + Jout += J_om[o][m] @ J_mi[m][i] + J_oi[o][i] = Jout + + return JacobianDict(J_oi, o_list, i_list) + + def apply(self, x): + # assume that all entries in x have some length T, and infer it + T = len(next(iter(x.values()))) + + inputs = x.keys() & set(self.inputs) + J_oi = self.complete().nesteddict + y = {} + + for o in self.outputs: + y[o] = np.zeros(T) + for i in inputs: + y[o] += J_oi[o][i] @ x[i] + + return y + + def pack(self, T): + J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) + for iO, O in enumerate(self.outputs): + for iI, I in enumerate(self.inputs): + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = support.make_matrix(self[O, I], T) + return J + + @staticmethod + def unpack(bigjac, outputs, inputs, T): + """If we have an (nO*T)*(nI*T) jacobian and provide names of nO outputs and nI inputs, output nested dictionary""" + jacdict = {} + for iO, O in enumerate(outputs): + jacdict[O] = {} + for iI, I in enumerate(inputs): + jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] + return JacobianDict(jacdict, outputs, inputs) + + +def ensure_valid_jacobiandict(d): + """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a + Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed + to be {}, which is permitted the empty version of a valid nested dict.""" + + if d != {} and not isinstance(d, JacobianDict): + # Assume it's sufficient to just check one of the keys + if not isinstance(next(iter(d.keys())), str): + raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") + + jac_o_dict = next(iter(d.values())) + if isinstance(jac_o_dict, dict): + if not isinstance(next(iter(jac_o_dict.keys())), str): + raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" + f" `input` names.") + jac_o_i = next(iter(jac_o_dict.values())) + if not isinstance(jac_o_i, Jacobian): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") + else: + if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: + raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") + else: + raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" + f" values of type `Jacobian`.") diff --git a/src/sequence_jacobian/jacobian/drivers.py b/src/sequence_jacobian/jacobian/drivers.py new file mode 100644 index 0000000..830554b --- /dev/null +++ b/src/sequence_jacobian/jacobian/drivers.py @@ -0,0 +1,261 @@ +"""Main methods (drivers) for computing and manipulating both block-level and model-level Jacobians""" + +import numpy as np + +from .classes import JacobianDict +from .support import pack_vectors, unpack_vectors +from ..utilities import misc, graph + +'''Drivers: + - get_H_U : get H_U matrix mapping all unknowns to all targets + - get_impulse : get single GE impulse response + - get_G : get G matrices characterizing all GE impulse responses + + - curlyJs_sorted : get block Jacobians curlyJ and return them topologically sorted + - forward_accumulate : forward accumulation on DAG, taking in topologically sorted Jacobians +''' + + +def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): + """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. + + Parameters + ---------- + blocks : list, simple blocks, het blocks, or jacdicts + unknowns : list of str, names of unknowns in DAG + targets : list of str, names of targets in DAG + T : int, truncation horizon + (if asymptotic, truncation horizon for backward iteration in HetBlocks) + ss : [optional] dict, steady state required if blocks contains any non-jacdicts + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians + + Returns + ------- + H_U : + if asymptotic=False: + array(T*n_u*T*n_u) H_U, Jacobian mapping all unknowns to all targets + is asymptotic=True: + array((2*Tpost-1)*n_u*n_u), representation of asymptotic columns of H_U + """ + + # do topological sort and get curlyJs + curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, Js) + + # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) + H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) + + # pack these n_u^2 matrices, each T*T, into a single matrix + return H_U_unpacked[targets, unknowns].pack(T) + + +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js=None): + """Get a single general equilibrium impulse response. + + Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed + and supplied to this function. Less so but still faster when H_U already computed. + + Parameters + ---------- + blocks : list, simple blocks or jacdicts + dZ : dict, path of an exogenous variable + unknowns : list of str, names of unknowns in DAG + targets : list of str, names of targets in DAG + T : [optional] int, truncation horizon + ss : [optional] dict, steady state required if blocks contains non-jacdicts + outputs : [optional] list of str, variables we want impulse responses for + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians + + Returns + ------- + out : dict, impulse responses to shock dZ + """ + # step 0 (preliminaries): infer T, do topological sort and get curlyJs + if T is None: + for x in dZ.values(): + T = len(x) + break + + curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, Js) + + # step 1: do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) + H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) + + # step 2: do (vector) forward accumulation to get J^(o, curlyZ)dZ for all o in + # 'alloutputs', the combination of outputs (if specified) and targets + alloutputs = None + if outputs is not None: + alloutputs = set(outputs) | set(targets) + + J_curlyZ_dZ = forward_accumulate(curlyJs, dZ, alloutputs, required) + + # step 3: solve H_UdU = -H_ZdZ for dU + H_U = H_U_unpacked[targets, unknowns].pack(T) + H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) + dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) + dU = unpack_vectors(dU_packed, unknowns, T) + + # step 4: do (vector) forward accumulation to get J^(o, curlyU)dU + # then sum together with J^(o, curlyZ)dZ to get all output impulse responses + J_curlyU_dU = forward_accumulate(curlyJs, dU, outputs, required) + if outputs is None: + outputs = J_curlyZ_dZ.keys() | J_curlyU_dU.keys() + return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} + + +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js=None): + """Compute Jacobians G that fully characterize general equilibrium outputs in response + to all exogenous shocks in 'exogenous' + + Faster when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed + and supplied to this function. Less so but still faster when H_U already computed. + Relative benefit of precomputing these not as extreme as for get_impulse, since + obtaining and solving with H_U is a less dominant component of cost for getting Gs. + + Parameters + ---------- + blocks : list, simple blocks or jacdicts + exogenous : list of str, names of exogenous shocks in DAG + unknowns : list of str, names of unknowns in DAG + targets : list of str, names of targets in DAG + T : [optional] int, truncation horizon + ss : [optional] dict, steady state required if blocks contains non-jacdicts + outputs : [optional] list of str, variables we want impulse responses for + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians + + Returns + ------- + G : dict of dict, Jacobians for general equilibrium mapping from exogenous to outputs + """ + + # step 1: do topological sort and get curlyJs + curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, Js) + + # step 2: do (matrix) forward accumulation to get + # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) + J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) + J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) + + # step 3: solve for G^U, unpack + H_U = J_curlyH_U[targets, unknowns].pack(T) + H_Z = J_curlyH_Z[targets, exogenous].pack(T) + + G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) + + # step 4: forward accumulation to get all outputs starting with G_U + # by default, don't calculate targets! + curlyJs = [G_U] + curlyJs + if outputs is None: + outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) + return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) + + +def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): + """ + Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs + and with respect to outputs of other blocks + + Parameters + ---------- + blocks : list, simple blocks or jacdicts + inputs : list, input names we need to differentiate with respect to + ss : [optional] dict, steady state, needed if blocks includes blocks themselves + T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians + + Returns + ------- + curlyJs : list of dict of dict, curlyJ for each block in order of topological sort + required : list, outputs of some blocks that are needed as inputs by others + """ + + # step 1: get topological sort and required + topsorted = graph.block_sort(blocks) + required = graph.find_outputs_that_are_intermediate_inputs(blocks) + + # Remove any vector-valued outputs that are intermediate inputs, since we don't want + # to compute Jacobians with respect to vector-valued variables + if ss is not None: + vv_vars = set([k for k, v in ss.items() if np.size(v) > 1]) + required -= vv_vars + + # step 2: compute Jacobians and put them in right order + curlyJs = [] + shocks = set(inputs) | required + for num in topsorted: + block = blocks[num] + jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() + if k in misc.input_kwarg_list(block.jacobian)}) + + # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) + # then don't add it to the list of curlyJs to be returned + if not jac: + continue + else: + curlyJs.append(JacobianDict(jac)) + + return curlyJs, required + + +def forward_accumulate(curlyJs, inputs, outputs=None, required=None): + """ + Use forward accumulation on topologically sorted Jacobians in curlyJs to get + all cumulative Jacobians with respect to 'inputs' if inputs is a list of names, + or get outcome of apply to 'inputs' if inputs is dict. + + Optionally only find outputs in 'outputs', especially if we have knowledge of + what is required for later Jacobians. + + Note that the overloading of @ means that this works automatically whether curlyJs are ordinary + matrices, simple_block.SimpleSparse objects, or asymptotic.AsymptoticTimeInvariant objects, + as long as the first and third are not mixed (since multiplication not defined for them). + + Much-extended version of chain_jacobians. + + Parameters + ---------- + curlyJs : list of dict of dict, curlyJ for each block in order of topological sort + inputs : list or dict, input names to differentiate with respect to, OR dict of input vectors + outputs : [optional] list or set, outputs we're interested in + required : [optional] list or set, outputs needed for later curlyJs (only useful w/outputs) + + Returns + ------- + out : dict of dict or dict, either total J for each output wrt all inputs or + outcome from applying all curlyJs + """ + + if outputs is not None and required is not None: + # if list of outputs provided, we need to obtain these and 'required' along the way + alloutputs = set(outputs) | set(required) + else: + # otherwise, set to None, implies default behavior of obtaining all outputs in curlyJs + alloutputs = None + + # if inputs is list (jacflag=True), interpret as list of inputs for which we want to calculate jacs + # if inputs is dict, interpret as input *paths* to which we apply all Jacobians in curlyJs + jacflag = not isinstance(inputs, dict) + + if jacflag: + # Jacobians of inputs with respect to themselves are the identity, initialize with this + # out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} + out = JacobianDict.identity(inputs) + else: + out = inputs.copy() + + # iterate through curlyJs, in what is presumed to be a topologically sorted order + for curlyJ in curlyJs: + curlyJ = JacobianDict(curlyJ).complete() + if alloutputs is not None: + # if we want specific list of outputs, restrict curlyJ to that before continuing + curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] + if jacflag: + out.update(curlyJ.compose(out)) + else: + out.update(curlyJ.apply(out)) + + if outputs is not None: + # if we want specific list of outputs, restrict to that + # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) + return out[[k for k in outputs if k in out.outputs]] + else: + return out diff --git a/src/sequence_jacobian/jacobian/support.py b/src/sequence_jacobian/jacobian/support.py new file mode 100644 index 0000000..8ab3b76 --- /dev/null +++ b/src/sequence_jacobian/jacobian/support.py @@ -0,0 +1,100 @@ +"""Various lower-level functions to support the computation of Jacobians""" + +import numpy as np +from numba import njit + + +# For supporting SimpleSparse +def multiply_basis(t1, t2): + """Matrix multiplication operation mapping two sparse basis elements to another.""" + # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with + # signs of i and j flipped to reflect different sign convention used here + i, m = t1 + j, n = t2 + k = i + j + if i >= 0: + if j >= 0: + l = max(m, n - i) + elif k >= 0: + l = max(m, n - k) + else: + l = max(m + k, n) + else: + if j <= 0: + l = max(m + j, n) + else: + l = max(m, n) + min(-i, j) + return k, l + + +def multiply_rs_rs(s1, s2): + """Matrix multiplication operation on two SimpleSparse objects.""" + # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, + # add all pairwise products to get overall product + elements = {} + for im, x in s1.elements.items(): + for jn, y in s2.elements.items(): + kl = multiply_basis(im, jn) + if kl in elements: + elements[kl] += x * y + else: + elements[kl] = x * y + return elements + + +@njit +def multiply_rs_matrix(indices, xs, A): + """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. + Much more computationally demanding than multiplying two SimpleSparse (which is almost + free with simple analytical formula), so we implement as jitted function.""" + n = indices.shape[0] + T = A.shape[0] + S = A.shape[1] + Aout = np.zeros((T, S)) + + for count in range(n): + # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' + # was stored in 'indices' and 'xs' + i = indices[count, 0] + m = indices[count, 1] + x = xs[count] + + # loop faster than vectorized when jitted + # directly use def of basis element (i, m), displacement of i and ignore first m + if i == 0: + for t in range(m, T): + for s in range(S): + Aout[t, s] += x * A[t, s] + elif i > 0: + for t in range(m, T - i): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + else: + for t in range(m - i, T): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + return Aout + + +def pack_vectors(vs, names, T): + v = np.zeros(len(names)*T) + for i, name in enumerate(names): + if name in vs: + v[i*T:(i+1)*T] = vs[name] + return v + + +def unpack_vectors(v, names, T): + vs = {} + for i, name in enumerate(names): + vs[name] = v[i*T:(i+1)*T] + return vs + + +def make_matrix(A, T): + """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method + to convert it to T*T array.""" + if not isinstance(A, np.ndarray): + return A.matrix(T) + else: + return A diff --git a/src/sequence_jacobian/models/__init__.py b/src/sequence_jacobian/models/__init__.py new file mode 100644 index 0000000..29259f6 --- /dev/null +++ b/src/sequence_jacobian/models/__init__.py @@ -0,0 +1 @@ +"""Specific Model Implementations""" \ No newline at end of file diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py new file mode 100644 index 0000000..8c2ce79 --- /dev/null +++ b/src/sequence_jacobian/models/hank.py @@ -0,0 +1,223 @@ +import numpy as np +from numba import vectorize, njit + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.het_block import het + + +'''Part 1: HA block''' + + +def household_init(a_grid, e_grid, r, w, eis, T): + fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + return fininc, Va + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, Pi_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): + """Single backward iteration step using endogenous gridpoint method for households with separable CRRA utility.""" + # this one is useful to do internally + ws = w * e_grid + + # uc(z_t, a_t) + uc_nextgrid = (beta * Pi_p) @ Va_p + + # c(z_t, a_t) and n(z_t, a_t) + c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) + + # c(z_t, a_{t-1}) and n(z_t, a_{t-1}) + lhs = c_nextgrid - ws[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] + rhs = (1 + r) * a_grid + c = utils.interpolate.interpolate_y(lhs, rhs, c_nextgrid) + n = utils.interpolate.interpolate_y(lhs, rhs, n_nextgrid) + + # test constraints, replace if needed + a = rhs + ws[:, np.newaxis] * n + T[:, np.newaxis] - c + iconst = np.nonzero(a < a_grid[0]) + a[iconst] = a_grid[0] + + # if there exist states/prior asset levels such that households want to borrow, compute the constrained + # solution for consumption and labor supply + if iconst[0].size != 0 and iconst[1].size != 0: + c[iconst], n[iconst] = solve_cn(ws[iconst[0]], rhs[iconst[1]] + T[iconst[0]] - a_grid[0], + eis, frisch, vphi, Va_p[iconst]) + + # calculate marginal utility to go backward + Va = (1 + r) * c ** (-1 / eis) + + # efficiency units of labor which is what really matters + n_e = e_grid[:, np.newaxis] * n + + return Va, a, c, n, n_e + + +def transfers(pi_e, Div, Tax, e_grid): + # default incidence rules are proportional to skill + tax_rule, div_rule = e_grid, e_grid # scale does not matter, will be normalized anyway + + div = Div / np.sum(pi_e * div_rule) * div_rule + tax = Tax / np.sum(pi_e * tax_rule) * tax_rule + T = div - tax + return T + + +household.add_hetinput(transfers, verbose=False) + + +@njit +def cn(uc, w, eis, frisch, vphi): + """Return optimal c, n as function of u'(c) given parameters""" + return uc ** (-eis), (w * uc / vphi) ** frisch + + +def solve_cn(w, T, eis, frisch, vphi, uc_seed): + uc = solve_uc(w, T, eis, frisch, vphi, uc_seed) + return cn(uc, w, eis, frisch, vphi) + + +@vectorize +def solve_uc(w, T, eis, frisch, vphi, uc_seed): + """Solve for optimal uc given in log uc space. + + max_{c, n} c**(1-1/eis) + vphi*n**(1+1/frisch) s.t. c = w*n + T + """ + log_uc = np.log(uc_seed) + for i in range(30): + ne, ne_p = netexp(log_uc, w, T, eis, frisch, vphi) + if abs(ne) < 1E-11: + break + else: + log_uc -= ne / ne_p + else: + raise ValueError("Cannot solve constrained household's problem: No convergence after 30 iterations!") + + return np.exp(log_uc) + + +@njit +def netexp(log_uc, w, T, eis, frisch, vphi): + """Return net expenditure as a function of log uc and its derivative.""" + c, n = cn(np.exp(log_uc), w, eis, frisch, vphi) + ne = c - w * n - T + + # c and n have elasticities of -eis and frisch wrt log u'(c) + c_loguc = -eis * c + n_loguc = frisch * n + netexp_loguc = c_loguc - w * n_loguc + + return ne, netexp_loguc + + +'''Part 2: Simple blocks and hetinput''' + + +@simple +def firm(Y, w, Z, pi, mu, kappa): + L = Y / Z + Div = Y - w * L - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y + return L, Div + + +@simple +def monetary(pi, rstar, phi): + r = (1 + rstar(-1) + phi * pi(-1)) / (1 + pi) - 1 + return r + + +@simple +def fiscal(r, B): + Tax = r * B + return Tax + + +@simple +def mkt_clearing(A, N_e, C, L, Y, B, pi, mu, kappa): + asset_mkt = A - B + labor_mkt = N_e - L + goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y + return asset_mkt, labor_mkt, goods_mkt + + +@simple +def nkpc(pi, w, Z, Y, r, mu, kappa): + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ + - (1 + pi).apply(np.log) + return nkpc_res + + +@simple +def income_state_vars(rho_s, sigma_s, nS): + e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) + return e_grid, pi_e, Pi + + +@simple +def asset_state_vars(amax, nA): + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return a_grid + + +@simple +def partial_steady_state_solution(B_Y, Y, mu, r, kappa, Z, pi): + B = B_Y + w = 1 / mu + Div = (1 - w) + Tax = r * B + + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) + + return B, w, Div, Tax, nkpc_res + + +'''Part 3: Steady state''' + + +def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1.2, B_Y=5.6, rho_s=0.966, sigma_s=0.5, + kappa=0.1, phi=1.5, nS=7, amax=150, nA=500): + """Solve steady state of full GE model. Calibrate (beta, vphi) to hit target for interest rate and Y.""" + + # set up grid + a_grid = utils.discretize.agrid(amax=amax, n=nA) + e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) + + # solve analytically what we can + B = B_Y + w = 1 / mu + Div = (1 - w) + Tax = r * B + T = transfers(pi_e, Div, Tax, e_grid) + + # initialize guess for policy function iteration + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + + # residual function + def res(x): + beta_loc, vphi_loc = x + # precompute constrained c and n which don't depend on Va + if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001: + raise ValueError('Clearly invalid inputs') + out = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta_loc, + eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc) + return np.array([out['A'] - B, out['N_e'] - 1]) + + # solve for beta, vphi + (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) + + # extra evaluation for reporting + ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis, + Div=Div, Tax=Tax, frisch=frisch, vphi=vphi) + + # check Walras's law + goods_mkt = 1 - ss['C'] + assert np.abs(goods_mkt) < 1E-8 + + # add aggregate variables + ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, + 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, + 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) + + return ss diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py new file mode 100644 index 0000000..82e4559 --- /dev/null +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -0,0 +1,125 @@ +import numpy as np +import scipy.optimize as opt + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.het_block import het + + +'''Part 1: HA block''' + + +def household_init(a_grid, e_grid, r, w, eis): + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + return Va + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, Pi_p, a_grid, e_grid, r, w, beta, eis): + """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. + + Parameters + ---------- + Va_p : array (S*A), marginal value of assets tomorrow + Pi_p : array (S*S), Markov matrix for skills tomorrow + a_grid : array (A), asset grid + e_grid : array (A), skill grid + r : scalar, ex-post real interest rate + w : scalar, wage + beta : scalar, discount rate today + eis : scalar, elasticity of intertemporal substitution + + Returns + ---------- + Va : array (S*A), marginal value of assets today + a : array (S*A), asset policy today + c : array (S*A), consumption policy today + """ + uc_nextgrid = (beta * Pi_p) @ Va_p + c_nextgrid = uc_nextgrid ** (-eis) + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) + utils.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + Va = (1 + r) * c ** (-1 / eis) + return Va, a, c + + +'''Part 2: Simple Blocks''' + + +@simple +def firm(K, L, Z, alpha, delta): + r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta + w = (1 - alpha) * Z * (K(-1) / L) ** alpha + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return r, w, Y + + +@simple +def mkt_clearing(K, A, Y, C, delta): + asset_mkt = A - K + goods_mkt = Y - C - delta * K + return asset_mkt, goods_mkt + + +@simple +def income_state_vars(rho, sigma, nS): + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + return e_grid, Pi + + +@simple +def asset_state_vars(amax, nA): + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return a_grid + + +@simple +def firm_steady_state_solution(r, delta, alpha): + rk = r + delta + Z = (rk / alpha) ** alpha # normalize so that Y=1 + K = (alpha * Z / rk) ** (1 / (1 - alpha)) + Y = Z * K ** alpha + w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) + + return Z, K, Y, w + + +'''Part 3: Steady state''' + + +def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, sigma=0.5, + nS=7, nA=500, amax=200): + """Solve steady state of full GE model. Calibrate beta to hit target for interest rate.""" + # set up grid + a_grid = utils.discretize.agrid(amax=amax, n=nA) + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + + # solve for aggregates analytically + rk = r + delta + Z = (rk / alpha) ** alpha # normalize so that Y=1 + K = (alpha * Z / rk) ** (1 / (1 - alpha)) + Y = Z * K ** alpha + w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) + + # figure out initializer + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + + # solve for beta consistent with this + beta_min = lb / (1 + r) + beta_max = ub / (1 + r) + beta, sol = opt.brentq(lambda bet: household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=bet, eis=eis, + Va=Va)['A'] - K, beta_min, beta_max, full_output=True) + if not sol.converged: + raise ValueError('Steady-state solver did not converge.') + + # extra evaluation to report variables + ss = household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=beta, eis=eis, Va=Va) + ss.update({'Pi': Pi, 'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, + 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, + 'rho': rho, 'nS': nS, 'asset_mkt': ss["A"] - K}) + + return ss diff --git a/src/sequence_jacobian/models/rbc.py b/src/sequence_jacobian/models/rbc.py new file mode 100644 index 0000000..61db8b0 --- /dev/null +++ b/src/sequence_jacobian/models/rbc.py @@ -0,0 +1,95 @@ +import numpy as np + +from ..blocks.simple_block import simple + +'''Part 1: Simple blocks''' + + +@simple +def firm(K, L, Z, alpha, delta): + r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta + w = (1 - alpha) * Z * (K(-1) / L) ** alpha + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return r, w, Y + + +@simple +def household(K, L, w, eis, frisch, vphi, delta): + C = (w / vphi / L ** (1 / frisch)) ** eis + I = K - (1 - delta) * K(-1) + return C, I + + +@simple +def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): + goods_mkt = Y - C - I + euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) + walras = C + K - (1 + r) * K(-1) - w * L + return goods_mkt, euler, walras + + +@simple +def steady_state_solution(Y, L, r, eis, delta, alpha, frisch): + # 1. Solve for beta to hit r + beta = 1 / (1 + r) + + # 2. Solve for K to hit goods_mkt + K = alpha * Y / (r + delta) + w = (1 - alpha) * Y / L + C = w * L + (1 + r) * K(-1) - K + I = delta * K + goods_mkt = Y - C - I + + # 3. Solve for Z to hit Y + Z = Y * K ** (-alpha) * L ** (alpha - 1) + + # 4. Solve for vphi to hit L + vphi = w * C ** (-1 / eis) * L ** (-1 / frisch) + + # 5. Have to return euler because it's a target + euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) + + return beta, K, w, C, I, goods_mkt, Z, vphi, euler + + +'''Part 2: Steady state''' + + +def rbc_ss(r=0.01, eis=1, frisch=1, delta=0.025, alpha=0.11): + """Solve steady state of simple RBC model. + + Parameters + ---------- + r : scalar, real interest rate + eis : scalar, elasticity of intertemporal substitution (1/sigma) + frisch : scalar, Frisch elasticity (1/nu) + delta : scalar, depreciation rate + alpha : scalar, capital share + + Returns + ------- + ss : dict, steady state values + """ + # solve for aggregates analytically + rk = r + delta + Z = (rk / alpha) ** alpha # normalize so that Y=1 + K = (alpha * Z / rk) ** (1 / (1 - alpha)) + Y = Z * K ** alpha + w = (1 - alpha) * Z * K ** alpha + I = delta * K + C = Y - I + + # preference params + beta = 1 / (1 + r) + vphi = w * C ** (-1 / eis) + + # check Walras's law, goods market clearing, and the euler equation + walras = C - r * K - w + goods_mkt = Y - C - I + euler = C ** (-1 / eis) - beta * (1 + r) * C ** (-1 / eis) + assert np.abs(walras) < 1E-12 + + return {'beta': beta, 'eis': eis, 'frisch': frisch, 'vphi': vphi, 'delta': delta, 'alpha': alpha, + 'Z': Z, 'K': K, 'I': I, 'Y': Y, 'L': 1, 'C': C, 'w': w, 'r': r, 'walras': walras, 'euler': euler, + 'goods_mkt': goods_mkt} + diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py new file mode 100644 index 0000000..403bdbe --- /dev/null +++ b/src/sequence_jacobian/models/two_asset.py @@ -0,0 +1,423 @@ +# pylint: disable=E1120 +import numpy as np +from numba import guvectorize + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.het_block import het, hetoutput +from ..blocks.solved_block import solved +from ..blocks.support.simple_displacement import apply_function + +'''Part 1: HA block''' + + +def household_init(b_grid, a_grid, e_grid, eis, tax, w): + z_grid = income(e_grid, tax, w, 1) + Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + + return z_grid, Va, Vb + + +@het(exogenous='Pi', policy=['b', 'a'], backward=['Vb', 'Va'], backward_init=household_init) # order as in grid! +def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): + # require that k is decreasing (new) + assert k_grid[1] < k_grid[0], 'kappas in k_grid must be decreasing!' + + # precompute Psi1(a', a) on grid of (a', a) for steps 3 and 5 + Psi1 = get_Psi_and_deriv(a_grid[:, np.newaxis], + a_grid[np.newaxis, :], ra, chi0, chi1, chi2)[1] + + # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === + # (take discounted expectation of tomorrow's value function) + Wb = matrix_times_first_dim(beta * Pi_p, Vb_p) + Wa = matrix_times_first_dim(beta * Pi_p, Va_p) + W_ratio = Wa / Wb + + # === STEP 3: a'(z, b', a) for UNCONSTRAINED === + + # for each (z, b', a), linearly interpolate to find a' between gridpoints + # satisfying optimality condition W_ratio == 1+Psi1 + i, pi = lhs_equals_rhs_interpolate(W_ratio, 1 + Psi1) + + # use same interpolation to get Wb and then c + a_endo_unc = utils.interpolate.apply_coord(i, pi, a_grid) + c_endo_unc = utils.interpolate.apply_coord(i, pi, Wb) ** (-eis) + + # === STEP 4: b'(z, b, a), a'(z, b, a) for UNCONSTRAINED === + + # solve out budget constraint to get b(z, b', a) + b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + + # interpolate this b' -> b mapping to get b -> b', so we have b'(z, b, a) + # and also use interpolation to get a'(z, b, a) + # (note utils.interpolate.interpolate_coord and utils.interpolate.apply_coord work on last axis, + # so we need to swap 'b' to the last axis, then back when done) + i, pi = utils.interpolate.interpolate_coord(b_endo.swapaxes(1, 2), b_grid) + a_unc = utils.interpolate.apply_coord(i, pi, a_endo_unc.swapaxes(1, 2)).swapaxes(1, 2) + b_unc = utils.interpolate.apply_coord(i, pi, b_grid).swapaxes(1, 2) + + # === STEP 5: a'(z, kappa, a) for CONSTRAINED === + + # for each (z, kappa, a), linearly interpolate to find a' between gridpoints + # satisfying optimality condition W_ratio/(1+kappa) == 1+Psi1, assuming b'=0 + lhs_con = W_ratio[:, 0:1, :] / (1 + k_grid[np.newaxis, :, np.newaxis]) + i, pi = lhs_equals_rhs_interpolate(lhs_con, 1 + Psi1) + + # use same interpolation to get Wb and then c + a_endo_con = utils.interpolate.apply_coord(i, pi, a_grid) + c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis]) ** (-eis) + * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) + + # === STEP 6: a'(z, b, a) for CONSTRAINED === + + # solve out budget constraint to get b(z, kappa, a), enforcing b'=0 + b_endo = (c_endo_con + a_endo_con + + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + + # interpolate this kappa -> b mapping to get b -> kappa + # then use the interpolated kappa to get a', so we have a'(z, b, a) + # (utils.interpolate.interpolate_y does this in one swoop, but since it works on last + # axis, we need to swap kappa to last axis, and then b back to middle when done) + a_con = utils.interpolate.interpolate_y(b_endo.swapaxes(1, 2), b_grid, + a_endo_con.swapaxes(1, 2)).swapaxes(1, 2) + + # === STEP 7: obtain policy functions and update derivatives of value function === + + # combine unconstrained solution and constrained solution, choosing latter + # when unconstrained goes below minimum b + a, b = a_unc.copy(), b_unc.copy() + b[b <= b_grid[0]] = b_grid[0] + a[b <= b_grid[0]] = a_con[b <= b_grid[0]] + + # calculate adjustment cost and its derivative + Psi, _, Psi2 = get_Psi_and_deriv(a, a_grid, ra, chi0, chi1, chi2) + + # solve out budget constraint to get consumption and marginal utility + c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b + uc = c ** (-1 / eis) + + # for GE wage Phillips curve we'll need endowment-weighted utility too + u = e_grid[:, np.newaxis, np.newaxis] * uc + + # update derivatives of value function using envelope conditions + Va = (1 + ra - Psi2) * uc + Vb = (1 + rb) * uc + + return Va, Vb, a, b, c, u + + +def income(e_grid, tax, w, N): + z_grid = (1 - tax) * w * N * e_grid + return z_grid + + +# A potential hetoutput to include with the above HetBlock +@hetoutput() +def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): + chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) + return chi + + +household.add_hetinput(income, verbose=False) +household.add_hetoutput(adjustment_costs, verbose=False) + + +"""Supporting functions for HA block""" + + +def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): + """Adjustment cost Psi(ap, a) and its derivatives with respect to + first argument (ap) and second argument (a)""" + a_with_return = (1 + ra) * a + a_change = ap - a_with_return + abs_a_change = np.abs(a_change) + sign_change = np.sign(a_change) + + adj_denominator = a_with_return + chi0 + core_factor = (abs_a_change / adj_denominator) ** (chi2 - 1) + + Psi = chi1 / chi2 * abs_a_change * core_factor + Psi1 = chi1 * sign_change * core_factor + Psi2 = -(1 + ra) * (Psi1 + (chi2 - 1) * Psi / adj_denominator) + return Psi, Psi1, Psi2 + + +def matrix_times_first_dim(A, X): + """Take matrix A times vector X[:, i1, i2, i3, ... , in] separately + for each i1, i2, i3, ..., in. Same output as A @ X if X is 1D or 2D""" + # flatten all dimensions of X except first, then multiply, then restore shape + return (A @ X.reshape(X.shape[0], -1)).reshape(X.shape) + + +def addouter(z, b, a): + """Take outer sum of three arguments: result[i, j, k] = z[i] + b[j] + a[k]""" + return z[:, np.newaxis, np.newaxis] + b[:, np.newaxis] + a + + +@guvectorize(['void(float64[:], float64[:,:], uint32[:], float64[:])'], '(ni),(ni,nj)->(nj),(nj)') +def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): + """ + Given lhs (i) and rhs (i,j), for each j, find the i such that + + lhs[i] > rhs[i,j] and lhs[i+1] < rhs[i+1,j] + + i.e. where given j, lhs == rhs in between i and i+1. + + Also return the pi such that + + pi*(lhs[i] - rhs[i,j]) + (1-pi)*(lhs[i+1] - rhs[i+1,j]) == 0 + + i.e. such that the point at pi*i + (1-pi)*(i+1) satisfies lhs == rhs by linear interpolation. + + If lhs[0] < rhs[0,j] already, just return u=0 and pi=1. + + ***IMPORTANT: Assumes that solution i is monotonically increasing in j + and that lhs - rhs is monotonically decreasing in i.*** + """ + + ni, nj = rhs.shape + assert len(lhs) == ni + + i = 0 + for j in range(nj): + while True: + if lhs[i] < rhs[i, j]: + break + elif i < nj - 1: + i += 1 + else: + break + + if i == 0: + iout[j] = 0 + piout[j] = 1 + else: + iout[j] = i - 1 + err_upper = rhs[i, j] - lhs[i] + err_lower = rhs[i - 1, j] - lhs[i - 1] + piout[j] = err_upper / (err_upper - err_lower) + + +'''Part 2: Simple blocks''' + + +@simple +def pricing(pi, mc, r, Y, kappap, mup): + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) \ + / (1 + r(+1)) - (1 + pi).apply(np.log) + return nkpc + + +@simple +def arbitrage(div, p, r): + equity = div(+1) + p(+1) - p * (1 + r(+1)) + return equity + + +@simple +def labor(Y, w, K, Z, alpha): + N = (Y / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) + mc = w * N / (1 - alpha) / Y + return N, mc + + +@simple +def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): + inv = (K / K(-1) - 1) / (delta * epsI) + 1 - Q + val = alpha * Z(+1) * (N(+1) / K) ** (1 - alpha) * mc(+1) - (K(+1) / K - (1 - delta) + (K(+1) / K - 1) ** 2 / ( + 2 * delta * epsI)) + K(+1) / K * Q(+1) - (1 + r(+1)) * Q + return inv, val + + +@simple +def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): + psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y + k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) + I = K - (1 - delta) * K(-1) + k_adjust + div = Y - w * N - I - psip + return psip, I, div + + +@simple +def taylor(rstar, pi, phi): + i = rstar + phi * pi + return i + + +@simple +def fiscal(r, w, N, G, Bg): + tax = (r * Bg + G) / w / N + return tax + + +@simple +def finance(i, p, pi, r, div, omega, pshare): + rb = r - omega + ra = pshare(-1) * (div + p) / p(-1) + (1 - pshare(-1)) * (1 + r) - 1 + fisher = 1 + i(-1) - (1 + r) * (1 + pi) + return rb, ra, fisher + + +@simple +def wage(pi, w): + piw = (1 + pi) * w / w(-1) - 1 + return piw + + +@simple +def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): + wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ + (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) + return wnkpc + + +@simple +def mkt_clearing(p, A, B, Bg, C, I, G, Chi, psip, omega, Y): + wealth = A + B + asset_mkt = p + Bg - wealth + goods_mkt = C + I + G + Chi + psip + omega * B - Y + return asset_mkt, wealth, goods_mkt + + +@simple +def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): + b_grid = utils.discretize.agrid(amax=bmax, n=nB) + a_grid = utils.discretize.agrid(amax=amax, n=nA) + k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) + + return b_grid, a_grid, k_grid, e_grid, Pi + + +@simple +def share_value(p, tot_wealth, Bh): + pshare = p / (tot_wealth - Bh) + return pshare + + +@simple +def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): + """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" + # 1. Solve for markup to hit total wealth + p = tot_wealth - Bg + mc = 1 - r * (p - K) / Y + mup = 1 / mc + wealth = tot_wealth + + # 2. Solve for capital share to hit K + alpha = (r + delta) * K / Y / mc + + # 3. Solve for TFP to hit N (Y cannot be used, because it is an unknown of the DAG) + Z = Y * K ** (-alpha) * N ** (alpha - 1) + + # 4. Solve for w such that piw = 0 + w = mc * (1 - alpha) * Y / N + piw = 0 + + return p, mc, mup, wealth, alpha, Z, w, piw + + +@simple +def partial_ss_step2(tax, w, U, N, muw, frisch): + """Solves for (vphi) to hit (wnkpc).""" + vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) + wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw + return vphi, wnkpc + + +'''Part 3: Steady state''' + + +def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, + muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi2=2, epsI=4, omega=0.005, kappaw=0.1, + phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, + verbose=True): + """Solve steady state of full GE model. Calibrate (beta, vphi, chi1, alpha, mup, Z) to hit targets for + (r, tot_wealth, Bh, K, Y=N=1). + """ + + # set up grid + b_grid = utils.discretize.agrid(amax=bmax, n=nB) + a_grid = utils.discretize.agrid(amax=amax, n=nA) + k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) + + # solve analytically what we can + I = delta * K + mc = 1 - r * (tot_wealth - Bg - K) + alpha = (r + delta) * K / mc + mup = 1 / mc + Z = K ** (-alpha) + w = (1 - alpha) * mc + tax = (r * Bg + G) / w + div = 1 - w - I + p = div / r + ra = r + rb = r - omega + + # figure out initializer + z_grid = income(e_grid, tax, w, 1) + Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + + # residual function + def res(x): + beta_loc, chi1_loc = x + if beta_loc > 0.999 / (1 + r) or chi1_loc < 0.5: + raise ValueError('Clearly invalid inputs') + out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, + k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) + asset_mkt = out['A'] + out['B'] - p - Bg + return np.array([asset_mkt, out['B'] - Bh]) + + # solve for beta, vphi, omega + (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), verbose=verbose) + + # extra evaluation to report variables + ss = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, + k_grid=k_grid, beta=beta, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1, chi2=chi2) + + # other things of interest + vphi = (1 - tax) * w * ss['U'] / muw + pshare = p / (tot_wealth - Bh) + + # calculate aggregate adjustment cost and check Walras's law + chi = get_Psi_and_deriv(ss.internal["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] + Chi = np.vdot(ss.internal["household"]['D'], chi) + goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 + + ss.internal["household"].update({"chi": chi}) + ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, + 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, + 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, + 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, + 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, + 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, 'goods_mkt': goods_mkt, + 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], + 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1 / mup), + 'wnkpc': kappaw * (vphi * ss["N"] ** (1 + 1 / frisch) - (1 - tax) * w * ss["N"] * ss["U"] / muw), + 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1 - alpha) * mc - delta - r}) + return ss + + +'''Part 4: Solved blocks for transition dynamics/Jacobian calculation''' + + +@solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver="brentq") +def pricing_solved(pi, mc, r, Y, kappap, mup): + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ + (1 + r(+1)) - (1 + pi).apply(np.log) + return nkpc + + +@solved(unknowns={'p': (5, 15)}, targets=['equity'], solver="brentq") +def arbitrage_solved(div, p, r): + equity = div(+1) + p(+1) - p * (1 + r(+1)) + return equity + + +production_solved = solved(block_list=[labor, investment], unknowns={'Q': 1., 'K': 10.}, + targets=['inv', 'val'], solver="broyden_custom") diff --git a/src/sequence_jacobian/nonlinear.py b/src/sequence_jacobian/nonlinear.py new file mode 100644 index 0000000..b45ef9e --- /dev/null +++ b/src/sequence_jacobian/nonlinear.py @@ -0,0 +1,112 @@ +"""Functions for solving for the non-linear transition dynamics provided a given shock path (e.g. solving MIT shocks)""" + +import numpy as np + +from .utilities import misc, graph +from .jacobian.drivers import get_H_U +from .jacobian.support import pack_vectors, unpack_vectors + + +def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=False, + returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): + """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. + + Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. + + Parameters + ---------- + block_list : list, blocks in model (SimpleBlocks or HetBlocks) + ss : dict, all steady-state information + exogenous : dict, all shocked Z go here, must all have same length T + unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) + targets : list, targets of SHADE DAG, the 'H' in H(U, Z) + Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians + monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k + (allows more efficient interpolation) + returnindividual: [optional] bool, flag to return individual outcomes from HetBlock.td + tol : [optional] scalar, for convergence of Newton's method we require |H| SteadyStateDict: + raise NotImplementedError(f'{type(self)} does not implement .steady_state()') + + def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: + raise NotImplementedError(f'{type(self)} does not implement .impulse_nonlinear()') + + def impulse_linear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: + raise NotImplementedError(f'{type(self)} does not implement .impulse_linear()') + + def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = None, + T: int = None, **kwargs) -> JacobianDict: + raise NotImplementedError(f'{type(self)} does not implement .jacobian()') + + def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], + unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], + targets: Union[Array, Dict[str, Union[str, Real]]], + solver: Optional[str] = "", **kwargs) -> SteadyStateDict: + """Evaluate a general equilibrium steady state of Block given a `calibration` + and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and + the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + solver = solver if solver else provide_solver_default(unknowns) + return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) + + def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], + unknowns: List[str], targets: List[str], + Js: Optional[Dict[str, JacobianDict]] = None, + **kwargs) -> ImpulseDict: + """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + irf_nonlin_gen_eq = td_solve(blocks, ss, + exogenous={k: v for k, v in exogenous.items()}, + unknowns=unknowns, targets=targets, Js=Js, **kwargs) + return ImpulseDict(irf_nonlin_gen_eq) + + def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], + exogenous: Dict[str, Array], + unknowns: List[str], targets: List[str], + T: Optional[int] = None, + Js: Optional[Dict[str, JacobianDict]] = None, + **kwargs) -> ImpulseDict: + """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks + from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) + return ImpulseDict(irf_lin_gen_eq) + + def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], + exogenous: List[str], + unknowns: List[str], targets: List[str], + T: Optional[int] = None, + Js: Optional[Dict[str, JacobianDict]] = None, + **kwargs) -> JacobianDict: + """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks + at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" + blocks = self.blocks if hasattr(self, "blocks") else [self] + return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) diff --git a/src/sequence_jacobian/steady_state/__init__.py b/src/sequence_jacobian/steady_state/__init__.py new file mode 100644 index 0000000..017e34b --- /dev/null +++ b/src/sequence_jacobian/steady_state/__init__.py @@ -0,0 +1 @@ +"""Steady state computation and support functions""" diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py new file mode 100644 index 0000000..cb8238f --- /dev/null +++ b/src/sequence_jacobian/steady_state/classes.py @@ -0,0 +1,66 @@ +"""Various classes to support the computation of steady states""" + +from copy import deepcopy + +from ..utilities.misc import dict_diff + + +class SteadyStateDict: + def __init__(self, data, internal=None): + self.toplevel = {} + self.internal = {} + self.update(data, internal_namespaces=internal) + + def __repr__(self): + if self.internal: + return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" + else: + return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" + + def __iter__(self): + return iter(self.toplevel) + + def __getitem__(self, k): + if isinstance(k, str): + return self.toplevel[k] + else: + try: + return {ki: self.toplevel[ki] for ki in k} + except TypeError: + raise TypeError(f'Key {k} needs to be a string or an iterable (list, set, etc) of strings') + + def __setitem__(self, k, v): + self.toplevel[k] = v + + def keys(self): + return self.toplevel.keys() + + def values(self): + return self.toplevel.values() + + def items(self): + return self.toplevel.items() + + def update(self, data, internal_namespaces=None): + if isinstance(data, SteadyStateDict): + self.internal.update(deepcopy(data.internal)) + self.toplevel.update(deepcopy(data.toplevel)) + else: + toplevel = deepcopy(data) + if internal_namespaces is not None: + # Construct the internal namespace from the Block object, if a Block is provided + if hasattr(internal_namespaces, "internal"): + internal_namespaces = {internal_namespaces.name: {k: v for k, v in deepcopy(data).items() if k in + internal_namespaces.internal}} + + # Remove the internal data from `data` if it's there + for internal_dict in internal_namespaces.values(): + toplevel = dict_diff(toplevel, internal_dict) + + self.toplevel.update(toplevel) + self.internal.update(internal_namespaces) + else: + self.toplevel.update(toplevel) + + def difference(self, data_to_remove): + return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py new file mode 100644 index 0000000..dbe6e9c --- /dev/null +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -0,0 +1,306 @@ +"""A general function for computing a model's steady state variables and parameters values""" + +import numpy as np +import scipy.optimize as opt +from copy import deepcopy +from functools import partial + +from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ + extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ + subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs +from .classes import SteadyStateDict +from ..utilities import solvers, graph, misc + + +# Find the steady state solution +def steady_state(blocks, calibration, unknowns, targets, dissolve=None, + sort_blocks=True, helper_blocks=None, helper_targets=None, + consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, + block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, + constrained_method="linear_continuation", constrained_kwargs=None): + """ + For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. + + blocks: `list` + A list of blocks, which include the types: SimpleBlock, HetBlock, SolvedBlock, CombinedBlock + calibration: `dict` + The pre-specified values of variables/parameters provided to the steady state computation + unknowns: `dict` + A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver + targets: `dict` + A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG + dissolve: `list` + A list of blocks, either SolvedBlock or CombinedBlock, where block-level unknowns are removed and subsumed + by the top-level unknowns, effectively removing the "solve" components of the blocks + sort_blocks: `bool` + Whether the blocks need to be topologically sorted (only False when this function is called from within a + Block object, like CombinedBlock, that has already pre-sorted the blocks) + helper_blocks: `list` + A list of blocks that replace some of the equations in the DAG to aid steady state calculation + helper_targets: `list` + A list of target names that are handled by the helper blocks + consistency_check: `bool` + If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values + without the assistance of helper blocks and see if the targets are still hit + ttol: `float` + The tolerance for the targets---how close the user wants the computed target values to equal the desired values + ctol: `float` + The tolerance for the consistency check---how close the user wants the computed target values, without the + use of helper blocks, to equal the desired values + fragile: `bool` + Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails + block_kwargs: `dict` + A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any + potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that + Block sub-class. + verbose: `bool` + Display the content of optional print statements within the solver for more responsive feedback + solver: `string` + The name of the numerical solver that the user would like to user. Can either be a custom solver the user + implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root + solver_kwargs: `dict` + The keyword arguments that the user's chosen solver requires to run + constrained_method: `str` + When using solvers that typically only take an initial value, x0, we provide a few options for manipulating + the solver to account for bounds when finding a solution. These methods are described in the + constrained_multivariate_residual function + constrained_kwargs: + The keyword arguments that the user's chosen constrained method requires to run + + return: ss_values: `dict` + A dictionary containing all of the pre-specified values and computed values from the steady state computation + """ + + dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ + instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, + block_kwargs, solver_kwargs, constrained_kwargs) + + # Initial setup of blocks, targets, and dictionary of steady state values to be returned + blocks_all = blocks + helper_blocks + targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + + helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) + helper_targets = {t: targets[t] for t in targets if t in helper_targets} + helper_outputs = {} + + ss_values = SteadyStateDict(calibration) + ss_values.update(helper_targets) + + if sort_blocks: + topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) + else: + topsorted = range(len(blocks + helper_blocks)) + + def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, consistency_check=False): + ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) + + # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper + # block subsetting + # Progress through the DAG computing the resulting steady state values based on the unknown_values + # provided to the residual function + for i in topsorted: + if not include_helpers and blocks_all[i] in helper_blocks: + continue + outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, + dissolve=dissolve, verbose=verbose, **block_kwargs) + if include_helpers and blocks_all[i] in helper_blocks: + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) + ss_values.update(outputs) + else: + # Don't overwrite entries in ss_values corresponding to what has already + # been solved for in helper_blocks so we can check for consistency after-the-fact + ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) + + # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the + # dict of ss_values to compute the "unknown_solutions" + return compute_target_values(targets_dict, ss_values) + + if helper_blocks: + unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, + helper_targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=ttol, verbose=verbose, fragile=fragile) + else: + unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=ttol, verbose=verbose, fragile=fragile) + + # Check that the solution is consistent with what would come out of the DAG without the helper blocks + if consistency_check and helper_blocks: + # Add the unknowns not handled by helpers into the DAG to be checked. + unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) + + cresid = abs(np.max(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), + include_helpers=False, consistency_check=True))) + run_consistency_check(cresid, ctol=ctol, fragile=fragile) + + # Update to set the solutions for the steady state values of the unknowns + ss_values.update(unknowns_solved) + + return ss_values + + +def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): + """Evaluate the .ss method of a block, given a dictionary of potential arguments""" + if toplevel_unknowns is None: + toplevel_unknowns = {} + block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False + + # Add the block's internal variables as inputs, if the block has an internal attribute + input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel + + # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them + # at the provided set of unknowns if included in dissolve. + valid_input_kwargs = misc.input_kwarg_list(block.steady_state) + input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} + if block in dissolve and "solver" in valid_input_kwargs: + input_kwarg_dict["solver"] = "solved" + input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} + elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: + raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," + f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," + f" {set(toplevel_unknowns)}.\n" + f"If the user provides a set of top-level unknowns that subsume block-level unknowns," + f" it must be explicitly declared in `dissolve`.") + + return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) + + +def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, + constrained_method="linear_continuation", constrained_kwargs=None, + tol=2e-12, verbose=False, fragile=False): + """ + Given a residual function (constructed within steady_state) and a set of bounds or initial values for + the set of unknowns, solve for the root. + TODO: Implemented as a hidden method as of now because this function relies on the structure of steady_state + specifically and will not work with a generic residual function, due to the way it currently expects residual + to call variables not provided as arguments explicitly but that exist in its enclosing scope. + + residual: `function` + A function to be supplied to a numerical solver that takes unknown values as arguments + and returns computed targets. + unknowns: `dict` + Refer to the `steady_state` function docstring for the "unknowns" variable + targets: `dict` + Refer to the `steady_state` function docstring for the "targets" variable + tol: `float` + The absolute convergence tolerance of the computed target to the desired target value in the numerical solver + solver: `str` + Refer to the `steady_state` function docstring for the "solver" variable + solver_kwargs: + Refer to the `steady_state` function docstring for the "solver_kwargs" variable + + return: The root[s] of the residual function as either a scalar (float) or a list of floats + """ + if residual_kwargs is None: + residual_kwargs = {} + + scipy_optimize_uni_solvers = ["bisect", "brentq", "brenth", "ridder", "toms748", "newton", "secant", "halley"] + scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", + "excitingmixing", "krylov", "df-sane"] + + # Construct a reduced residual function, which contains addl context of unknowns, targets, and keyword arguments. + # This is to bypass issues with passing a residual function that requires contextual, positional arguments + # separate from the unknown values that need to be solved for into the multivariate solvers + residual_f = partial(residual, targets, unknowns.keys(), **residual_kwargs) + + if solver is None: + raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") + elif solver in scipy_optimize_uni_solvers: + initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) + result = opt.root_scalar(residual_f, method=solver, xtol=tol, + **initial_values_or_bounds, **solver_kwargs) + if not result.converged: + raise ValueError(f"Steady-state solver, {solver}, did not converge.") + unknown_solutions = result.root + elif solver in scipy_optimize_multi_solvers: + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns, fragile=fragile) + # If no bounds were provided + if not bounds: + result = opt.root(residual_f, initial_values, + method=solver, tol=tol, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + result = opt.root(constrained_residual, initial_values, + method=solver, tol=tol, **solver_kwargs) + if not result.success: + raise ValueError(f"Steady-state solver, {solver}, did not converge." + f" The termination status is {result.status}.") + unknown_solutions = list(result.x) + # TODO: Implement a more general interface for custom solvers, so we don't need to add new elifs at this level + # everytime a new custom solver is implemented. + elif solver == "broyden_custom": + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + # If no bounds were provided + if not bounds: + unknown_solutions, _ = solvers.broyden_solver(residual_f, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, + verbose=verbose, tol=tol, **solver_kwargs) + unknown_solutions = list(unknown_solutions) + elif solver == "newton_custom": + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + # If no bounds were provided + if not bounds: + unknown_solutions, _ = solvers.newton_solver(residual_f, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + unknown_solutions = list(unknown_solutions) + elif solver == "solved": + # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution + # simply call residual_f once to populate the `ss_values` dict + residual_f(unknowns.values()) + unknown_solutions = unknowns.values() + else: + raise RuntimeError(f"steady_state is not yet compatible with {solver}.") + + return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) + + +def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, + solver, solver_kwargs, constrained_method="linear_continuation", + constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): + """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets + with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" + # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, + # to populate the `ss_values` dict with the unknown values that: + # a) are handled by helper blocks and b) are excludable from the main DAG + # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed + unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] + targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) + + # Subset out the unknowns and targets that are not excludable from the main DAG loop + unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) + targets_non_excl = misc.dict_diff(targets, helper_targets) + + # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of + # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and + # we can omit them and their corresponding `unknowns` in the main DAG. + if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): + unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, + solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=tol, verbose=verbose, fragile=fragile) + # If targets handled by helpers and excludable from the main DAG are not satisfied then + # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, + # and they remain a part of the main DAG when solving. + else: + unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=tol, verbose=verbose, fragile=fragile) + return unknown_solutions diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py new file mode 100644 index 0000000..88374b6 --- /dev/null +++ b/src/sequence_jacobian/steady_state/support.py @@ -0,0 +1,253 @@ +"""Various lower-level functions to support the computation of steady states""" + +import warnings +from numbers import Real +import numpy as np + + +def instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, + block_kwargs, solver_kwargs, constrained_kwargs): + """Instantiate mutable types from `None` default values in the steady_state function""" + if dissolve is None: + dissolve = [] + if helper_blocks is None and helper_targets is None: + helper_blocks = [] + helper_targets = [] + elif helper_blocks is not None and helper_targets is None: + raise ValueError("If the user has provided `helper_blocks`, the kwarg `helper_targets` must be specified" + " indicating which target variables are handled by the `helper_blocks`.") + elif helper_blocks is None and helper_targets is not None: + raise ValueError("If the user has provided `helper_targets`, the kwarg `helper_blocks` must be specified" + " indicating which helper blocks handle the `helper_targets`") + if block_kwargs is None: + block_kwargs = {} + if solver_kwargs is None: + solver_kwargs = {} + if constrained_kwargs is None: + constrained_kwargs = {} + + return dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs + + +def provide_solver_default(unknowns): + if len(unknowns) == 1: + bounds = list(unknowns.values())[0] + if not isinstance(bounds, tuple) or bounds[0] > bounds[1]: + raise ValueError("Unable to find a compatible one-dimensional solver with provided `unknowns`.\n" + " Please provide valid lower/upper bounds, e.g. unknowns = {`a`: (0, 1)}") + else: + return "brentq" + elif len(unknowns) > 1: + init_values = list(unknowns.values()) + if not np.all([isinstance(v, Real) for v in init_values]): + raise ValueError("Unable to find a compatible multi-dimensional solver with provided `unknowns`.\n" + " Please provide valid initial values, e.g. unknowns = {`a`: 1, `b`: 2}") + else: + return "broyden_custom" + else: + raise ValueError("`unknowns` is empty! Please provide a dict of keys/values equal to the number of unknowns" + " that need to be solved for.") + + +def run_consistency_check(cresid, ctol=1e-9, fragile=False): + if cresid > ctol: + if fragile: + raise RuntimeError(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + else: + warnings.warn(f"The target values evaluated for the proposed set of unknowns produce a " + f"maximum residual value of {cresid}, which is greater than the ctol {ctol}.\n" + f" If used, check if HelperBlocks are indeed compatible with the DAG.\n" + f" If this is not an issue, adjust ctol accordingly.") + + +# Allow targets to be specified in the following formats +# 1) target = {"asset_mkt": 0} or ["asset_mkt"] (the standard case, where the target = 0) +# 2) target = {"r": 0.01} (allowing for the target to be non-zero) +# 3) target = {"K": "A"} (allowing the target to be another variable in potential_args) +def compute_target_values(targets, potential_args): + """ + For a given set of target specifications and potential arguments available, compute the targets. + Called as the return value for the residual function when utilizing the numerical solver. + + targets: Refer to `steady_state` function docstring + potential_args: Refer to the `steady_state` function docstring for the "calibration" variable + + return: A `float` (if computing a univariate target) or an `np.ndarray` (if using a multivariate target) + """ + target_values = np.empty(len(targets)) + for (i, t) in enumerate(targets): + v = targets[t] if isinstance(targets, dict) else 0 + if type(v) == str: + target_values[i] = potential_args[t] - potential_args[v] + else: + target_values[i] = potential_args[t] - v + # TODO: Implement feature to allow for an arbitrary explicit function expression as a potential target value + # e.g. targets = {"goods_mkt": "Y - C - I"}, so long as the expression is only comprise of generic numerical + # operators and variables solved for along the DAG prior to reaching the target. + + # Univariate solvers require float return values (and not lists) + if len(targets) == 1: + return target_values[0] + else: + return target_values + + +def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): + """Find the set of unknowns that the `helper_blocks` solve for""" + unknowns_handled_by_helpers = {} + for block in helper_blocks: + unknowns_handled_by_helpers.update({u: unknowns_all[u] for u in block.outputs if u in unknowns_all}) + + n_unknowns = len(unknowns_handled_by_helpers) + n_targets = len(helper_targets) + if n_unknowns != n_targets: + raise ValueError(f"The provided helper_blocks handle {n_unknowns} unknowns != {n_targets} targets." + f" User must specify an equal number of unknowns/targets solved for by helper blocks.") + + return unknowns_handled_by_helpers + + +def find_excludable_helper_blocks(blocks_all, helper_indices, helper_unknowns, helper_targets): + """Of the set of helper_unknowns and helper_targets, find the ones that can be excluded from the main DAG + for the purposes of numerically solving unknowns.""" + excludable_helper_unknowns = {} + excludable_helper_targets = {} + for i in helper_indices: + excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) + excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) + return excludable_helper_unknowns, excludable_helper_targets + + +def extract_univariate_initial_values_or_bounds(unknowns): + val = next(iter(unknowns.values())) + if np.isscalar(val): + return {"x0": val} + else: + return {"bracket": (val[0], val[1])} + + +def extract_multivariate_initial_values_and_bounds(unknowns, fragile=False): + """Provided a dict mapping names of unknowns to initial values/bounds, return separate dicts of + the initial values and bounds. + Note: For one-sided bounds, simply put np.inf/-np.inf as the other side of the bounds, so there is + no ambiguity about which is the unconstrained side. +""" + initial_values = [] + multi_bounds = {} + for k, v in unknowns.items(): + if np.isscalar(v): + initial_values.append(v) + elif len(v) == 2: + if fragile: + raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." + f" the values of `unknowns` must either be a scalar, pertaining to a" + f" single initial value for the root solver to begin from," + f" a length 2 tuple, pertaining to a lower bound and an upper bound," + f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") + else: + warnings.warn("Interpreting values of `unknowns` from length 2 tuple as lower and upper bounds" + " and averaging them to get a scalar initial value to provide to the solver.") + initial_values.append((v[0] + v[1])/2) + elif len(v) == 3: + lb, iv, ub = v + assert lb < iv < ub + initial_values.append(iv) + multi_bounds[k] = (lb, ub) + else: + raise ValueError(f"{len(v)} is an invalid size for the value of an unknown." + f" the values of `unknowns` must either be a scalar, pertaining to a" + f" single initial value for the root solver to begin from," + f" a length 2 tuple, pertaining to a lower bound and an upper bound," + f" or a length 3 tuple, pertaining to a lower bound, initial value, and upper bound.") + + return np.asarray(initial_values), multi_bounds + + +def residual_with_linear_continuation(residual, bounds, eval_at_boundary=False, + boundary_epsilon=1e-4, penalty_scale=1e1, + verbose=False): + """Modify a residual function to implement bounds by an additive penalty for exceeding the boundaries + provided, scaled by the amount the guess exceeds the boundary. + + e.g. For residual function f(x), desiring x in (0, 1) (so assuming eval_at_boundary = False) + If the guess for x is 1.1 then we will censor to x_censored = 1 - boundary_epsilon, and return + f(x_censored) + penalty (where the penalty does not require re-evaluating f() which may be costly) + + residual: `function` + The function whose roots we want to solve for + bounds: `dict` + A dict mapping the names of the unknowns (`str`) to length two tuples corresponding to the lower and upper + bounds. + eval_at_boundary: `bool` + Whether to allow the residual function to be evaluated at exactly the boundary values or not. + Think of it as whether the solver will treat the bounds as creating a closed or open set for the search space. + boundary_epsilon: `float` + The amount to adjust the proposed guess, x, by to calculate the censored value of the residual function, + when the proposed guess exceeds the boundaries. + penalty_scale: `float` + The linear scaling factor for adjusting the penalty for the proposed unknown values exceeding the boundary. + verbose: `bool` + Whether to print out additional information for how the constrained residual function is behaving during + optimization. Useful for tuning the solver. + """ + lbs = np.asarray([v[0] for v in bounds.values()]) + ubs = np.asarray([v[1] for v in bounds.values()]) + + def constr_residual(x, residual_cache=[]): + """Implements a constrained residual function, where any attempts to evaluate x outside of the + bounds provided will result in a linear penalty function scaled by `penalty_scale`. + + Note: We are purposefully using residual_cache as a mutable default argument to cache the most recent + valid evaluation (maintain state between function calls) of the residual function to induce solvers + to backstep if they encounter a region of the search space that returns nan values. + See Hitchhiker's Guide to Python post on Mutable Default Arguments: "When the Gotcha Isn't a Gotcha" + """ + if eval_at_boundary: + x_censored = np.where(x < lbs, lbs, x) + x_censored = np.where(x > ubs, ubs, x_censored) + else: + x_censored = np.where(x < lbs, lbs + boundary_epsilon, x) + x_censored = np.where(x > ubs, ubs - boundary_epsilon, x_censored) + + residual_censored = residual(x_censored) + + if verbose: + print(f"Attempted x is {x}") + print(f"Censored x is {x_censored}") + print(f"The residual_censored is {residual_censored}") + + if np.any(np.isnan(residual_censored)): + # Provide a scaled penalty to the solver when trying to evaluate residual() in an undefined region + residual_censored = residual_cache[0] * penalty_scale + + if verbose: + print(f"The new residual_censored is {residual_censored}") + else: + if not residual_cache: + residual_cache.append(residual_censored) + else: + residual_cache[0] = residual_censored + + if verbose: + print(f"The residual_cache is {residual_cache[0]}") + + # Provide an additive, scaled penalty to the solver when trying to evaluate residual() outside of the boundary + residual_with_boundary_penalty = residual_censored + \ + (x - x_censored) * penalty_scale * residual_censored + return residual_with_boundary_penalty + + return constr_residual + + +def constrained_multivariate_residual(residual, bounds, method="linear_continuation", verbose=False, + **constrained_kwargs): + """Return a constrained version of the residual function, which accounts for bounds, using the specified method. + See the docstring of the specific method of interest for further details.""" + if method == "linear_continuation": + return residual_with_linear_continuation(residual, bounds, verbose=verbose, **constrained_kwargs) + # TODO: Implement logistic transform as another option for constrained multivariate residual + else: + raise ValueError(f"Method {method} for constrained multivariate root-finding has not yet been implemented.") diff --git a/src/sequence_jacobian/utilities/__init__.py b/src/sequence_jacobian/utilities/__init__.py new file mode 100644 index 0000000..e8d1bab --- /dev/null +++ b/src/sequence_jacobian/utilities/__init__.py @@ -0,0 +1,3 @@ +"""Utilities relating to: interpolation, forward step/transition, grids and Markov chains, solvers, sorting, etc.""" + +from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers diff --git a/src/sequence_jacobian/utilities/differentiate.py b/src/sequence_jacobian/utilities/differentiate.py new file mode 100644 index 0000000..758c67e --- /dev/null +++ b/src/sequence_jacobian/utilities/differentiate.py @@ -0,0 +1,56 @@ +"""Numerical differentiation""" + +from .misc import make_tuple + + +def numerical_diff(func, ssinputs_dict, shock_dict, h=1E-4, y_ss_list=None): + """Differentiate function numerically via forward difference, i.e. calculate + + f'(xss)*shock = (f(xss + h*shock) - f(xss))/h + + for small h. (Variable names inspired by application of differentiating around ss.) + + Parameters + ---------- + func : function, 'f' to be differentiated + ssinputs_dict : dict, values in 'xss' around which to differentiate + shock_dict : dict, values in 'shock' for which we're taking derivative + (keys in shock_dict are weak subset of keys in ssinputs_dict) + h : [optional] scalar, scaling of forward difference 'h' + y_ss_list : [optional] list, value of y=f(xss) if we already have it + + Returns + ---------- + dy_list : list, output f'(xss)*shock of numerical differentiation + """ + # compute ss output if not supplied + if y_ss_list is None: + y_ss_list = make_tuple(func(**ssinputs_dict)) + + # response to small shock + shocked_inputs = {**ssinputs_dict, **{k: ssinputs_dict[k] + h * shock for k, shock in shock_dict.items()}} + y_list = make_tuple(func(**shocked_inputs)) + + # scale responses back up, dividing by h + dy_list = [(y - y_ss) / h for y, y_ss in zip(y_list, y_ss_list)] + + return dy_list + + +def numerical_diff_symmetric(func, ssinputs_dict, shock_dict, h=1E-4): + """Same as numerical_diff, but differentiate numerically using central (symmetric) difference, i.e. + + f'(xss)*shock = (f(xss + h*shock) - f(xss - h*shock))/(2*h) + """ + + # response to small shock in each direction + shocked_inputs_up = {**ssinputs_dict, **{k: ssinputs_dict[k] + h * shock for k, shock in shock_dict.items()}} + y_up_list = make_tuple(func(**shocked_inputs_up)) + + shocked_inputs_down = {**ssinputs_dict, **{k: ssinputs_dict[k] - h * shock for k, shock in shock_dict.items()}} + y_down_list = make_tuple(func(**shocked_inputs_down)) + + # scale responses back up, dividing by h + dy_list = [(y_up - y_down) / (2*h) for y_up, y_down in zip(y_up_list, y_down_list)] + + return dy_list diff --git a/src/sequence_jacobian/utilities/discretize.py b/src/sequence_jacobian/utilities/discretize.py new file mode 100644 index 0000000..95ace8f --- /dev/null +++ b/src/sequence_jacobian/utilities/discretize.py @@ -0,0 +1,139 @@ +"""Grids and Markov chains""" + +import numpy as np +from scipy.stats import norm + + +def agrid(amax, n, amin=0): + """Create grid between amin-pivot and amax+pivot that is equidistant in logs.""" + pivot = np.abs(amin) + 0.25 + a_grid = np.geomspace(amin + pivot, amax + pivot, n) - pivot + a_grid[0] = amin # make sure *exactly* equal to amin + return a_grid + + +# TODO: Temporarily include the old way of constructing grids from ikc_old for comparability of results +def agrid_old(amax, N, amin=0, frac=1/25): + """crappy discretization method we've been using, generates N point + log-spaced grid between bmin and bmax, choosing pivot such that 'frac' of + total log space between log(1+amin) and log(1+amax) beneath it""" + apivot = (1+amin)**(1-frac)*(1+amax)**frac - 1 + a = np.geomspace(amin+apivot,amax+apivot,N) - apivot + a[0] = amin + return a + + +def stationary(Pi, pi_seed=None, tol=1E-11, maxit=10_000): + """Find invariant distribution of a Markov chain by iteration.""" + if pi_seed is None: + pi = np.ones(Pi.shape[0]) / Pi.shape[0] + else: + pi = pi_seed + + for it in range(maxit): + pi_new = pi @ Pi + if np.max(np.abs(pi_new - pi)) < tol: + break + pi = pi_new + else: + raise ValueError(f'No convergence after {maxit} forward iterations!') + pi = pi_new + + return pi + + +def mean(x, pi): + """Mean of discretized random variable with support x and probability mass function pi.""" + return np.sum(pi * x) + + +def variance(x, pi): + """Variance of discretized random variable with support x and probability mass function pi.""" + return np.sum(pi * (x - np.sum(pi * x)) ** 2) + + +def std(x, pi): + """Standard deviation of discretized random variable with support x and probability mass function pi.""" + return np.sqrt(variance(x, pi)) + + +def cov(x, y, pi): + """Covariance of two discretized random variables with supports x and y common probability mass function pi.""" + return np.sum(pi * (x - mean(x, pi)) * (y - mean(y, pi))) + + +def corr(x, y, pi): + """Correlation of two discretized random variables with supports x and y common probability mass function pi.""" + return cov(x, y, pi) / (std(x, pi) * std(y, pi)) + + +def markov_tauchen(rho, sigma, N=7, m=3, normalize=True): + """Tauchen method discretizing AR(1) s_t = rho*s_(t-1) + eps_t. + + Parameters + ---------- + rho : scalar, persistence + sigma : scalar, unconditional sd of s_t + N : int, number of states in discretized Markov process + m : scalar, discretized s goes from approx -m*sigma to m*sigma + + Returns + ---------- + y : array (N), states proportional to exp(s) s.t. E[y] = 1 + pi : array (N), stationary distribution of discretized process + Pi : array (N*N), Markov matrix for discretized process + """ + + # make normalized grid, start with cross-sectional sd of 1 + s = np.linspace(-m, m, N) + ds = s[1] - s[0] + sd_innov = np.sqrt(1 - rho ** 2) + + # standard Tauchen method to generate Pi given N and m + Pi = np.empty((N, N)) + Pi[:, 0] = norm.cdf(s[0] - rho * s + ds / 2, scale=sd_innov) + Pi[:, -1] = 1 - norm.cdf(s[-1] - rho * s - ds / 2, scale=sd_innov) + for j in range(1, N - 1): + Pi[:, j] = (norm.cdf(s[j] - rho * s + ds / 2, scale=sd_innov) - + norm.cdf(s[j] - rho * s - ds / 2, scale=sd_innov)) + + # invariant distribution and scaling + pi = stationary(Pi) + s *= (sigma / np.sqrt(variance(s, pi))) + if normalize: + y = np.exp(s) / np.sum(pi * np.exp(s)) + else: + y = s + + return y, pi, Pi + + +def markov_rouwenhorst(rho, sigma, N=7): + """Rouwenhorst method analog to markov_tauchen""" + + # Explicitly typecast N as an integer, since when the grid constructor functions + # (e.g. the function that makes a_grid) are implemented as blocks, they interpret the integer-valued calibration + # as a float. + N = int(N) + + # parametrize Rouwenhorst for n=2 + p = (1 + rho) / 2 + Pi = np.array([[p, 1 - p], [1 - p, p]]) + + # implement recursion to build from n=3 to n=N + for n in range(3, N + 1): + P1, P2, P3, P4 = (np.zeros((n, n)) for _ in range(4)) + P1[:-1, :-1] = p * Pi + P2[:-1, 1:] = (1 - p) * Pi + P3[1:, :-1] = (1 - p) * Pi + P4[1:, 1:] = p * Pi + Pi = P1 + P2 + P3 + P4 + Pi[1:-1] /= 2 + + # invariant distribution and scaling + pi = stationary(Pi) + s = np.linspace(-1, 1, N) + s *= (sigma / np.sqrt(variance(s, pi))) + y = np.exp(s) / np.sum(pi * np.exp(s)) + + return y, pi, Pi diff --git a/src/sequence_jacobian/utilities/forward_step.py b/src/sequence_jacobian/utilities/forward_step.py new file mode 100644 index 0000000..96ffa3b --- /dev/null +++ b/src/sequence_jacobian/utilities/forward_step.py @@ -0,0 +1,175 @@ +"""Forward iteration of distribution on grid and related functions. + + - forward_step_1d + - forward_step_2d + - apply law of motion for distribution to go from D_{t-1} to D_t + + - forward_step_shock_1d + - forward_step_shock_2d + - forward_step linearized, used in part 1 of fake news algorithm to get curlyDs + + - forward_step_transpose_1d + - forward_step_transpose_2d + - transpose of forward_step, used in part 2 of fake news algorithm to get curlyPs +""" + +import numpy as np +from numba import njit + + +@njit +def forward_step_1d(D, Pi_T, x_i, x_pi): + """Single forward step to update distribution using exogenous Markov transition Pi and + policy x_i and x_pi for one-dimensional endogenous state. + + Efficient implementation of D_t = Lam_{t-1}' @ D_{t-1} using sparsity of the endogenous + part of Lam_{t-1}'. + + Note that it takes Pi_T, the transpose of Pi, as input rather than transposing itself; + this is so that when it is applied repeatedly, we can precalculate a transpose stored in + correct order rather than a view. + + Parameters + ---------- + D : array (S*X), beginning-of-period distribution over s_t, x_(t-1) + Pi_T : array (S*S), transpose Markov matrix that maps s_t to s_(t+1) + x_i : int array (S*X), left gridpoint of endogenous policy + x_pi : array (S*X), weight on left gridpoint of endogenous policy + + Returns + ---------- + Dnew : array (S*X), beginning-of-next-period dist s_(t+1), x_t + """ + + # first update using endogenous policy + nZ, nX = D.shape + Dnew = np.zeros_like(D) + for iz in range(nZ): + for ix in range(nX): + i = x_i[iz, ix] + pi = x_pi[iz, ix] + d = D[iz, ix] + Dnew[iz, i] += d * pi + Dnew[iz, i+1] += d * (1 - pi) + + # then using exogenous transition matrix + return Pi_T @ Dnew + + +def forward_step_2d(D, Pi_T, x_i, y_i, x_pi, y_pi): + """Like forward_step_1d but with two-dimensional endogenous state, policies given by x and y""" + Dmid = forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi) + nZ, nX, nY = Dmid.shape + return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) + + +@njit +def forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi): + """Endogenous update part of forward_step_2d""" + nZ, nX, nY = D.shape + Dnew = np.zeros_like(D) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i[iz, ix, iy] + iyp = y_i[iz, ix, iy] + beta = x_pi[iz, ix, iy] + alpha = y_pi[iz, ix, iy] + + Dnew[iz, ixp, iyp] += alpha * beta * D[iz, ix, iy] + Dnew[iz, ixp+1, iyp] += alpha * (1 - beta) * D[iz, ix, iy] + Dnew[iz, ixp, iyp+1] += (1 - alpha) * beta * D[iz, ix, iy] + Dnew[iz, ixp+1, iyp+1] += (1 - alpha) * (1 - beta) * D[iz, ix, iy] + return Dnew + + +@njit +def forward_step_shock_1d(Dss, Pi_T, x_i_ss, x_pi_shock): + """forward_step_1d linearized wrt x_pi""" + # first find effect of shock to endogenous policy + nZ, nX = Dss.shape + Dshock = np.zeros_like(Dss) + for iz in range(nZ): + for ix in range(nX): + i = x_i_ss[iz, ix] + dshock = x_pi_shock[iz, ix] * Dss[iz, ix] + Dshock[iz, i] += dshock + Dshock[iz, i + 1] -= dshock + + # then apply exogenous transition matrix to update + return Pi_T @ Dshock + + +def forward_step_shock_2d(Dss, Pi_T, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): + """forward_step_2d linearized wrt x_pi and y_pi""" + Dmid = forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock) + nZ, nX, nY = Dmid.shape + return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) + + +@njit +def forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): + """Endogenous update part of forward_step_shock_2d""" + nZ, nX, nY = Dss.shape + Dshock = np.zeros_like(Dss) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i_ss[iz, ix, iy] + iyp = y_i_ss[iz, ix, iy] + alpha = x_pi_ss[iz, ix, iy] + beta = y_pi_ss[iz, ix, iy] + + dalpha = x_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] + dbeta = y_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] + + Dshock[iz, ixp, iyp] += dalpha * beta + alpha * dbeta + Dshock[iz, ixp+1, iyp] += dbeta * (1-alpha) - beta * dalpha + Dshock[iz, ixp, iyp+1] += dalpha * (1-beta) - alpha * dbeta + Dshock[iz, ixp+1, iyp+1] -= dalpha * (1-beta) + dbeta * (1-alpha) + return Dshock + + +@njit +def forward_step_transpose_1d(D, Pi, x_i, x_pi): + """Transpose of forward_step_1d""" + # first update using exogenous transition matrix + D = Pi @ D + + # then update using (transpose) endogenous policy + nZ, nX = D.shape + Dnew = np.zeros_like(D) + for iz in range(nZ): + for ix in range(nX): + i = x_i[iz, ix] + pi = x_pi[iz, ix] + Dnew[iz, ix] = pi * D[iz, i] + (1-pi) * D[iz, i+1] + return Dnew + + +def forward_step_transpose_2d(D, Pi, x_i, y_i, x_pi, y_pi): + """Transpose of forward_step_2d.""" + nZ, nX, nY = D.shape + Dmid = (Pi @ D.reshape(nZ, -1)).reshape(nZ, nX, nY) + return forward_step_transpose_endo_2d(Dmid, x_i, y_i, x_pi, y_pi) + + +@njit +def forward_step_transpose_endo_2d(D, x_i, y_i, x_pi, y_pi): + """Endogenous update part of forward_step_transpose_2d""" + nZ, nX, nY = D.shape + Dnew = np.empty_like(D) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i[iz, ix, iy] + iyp = y_i[iz, ix, iy] + alpha = x_pi[iz, ix, iy] + beta = y_pi[iz, ix, iy] + + Dnew[iz, ix, iy] = (alpha * beta * D[iz, ixp, iyp] + alpha * (1-beta) * D[iz, ixp, iyp+1] + + (1-alpha) * beta * D[iz, ixp+1, iyp] + + (1-alpha) * (1-beta) * D[iz, ixp+1, iyp+1]) + return Dnew + + diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py new file mode 100644 index 0000000..aa01ecd --- /dev/null +++ b/src/sequence_jacobian/utilities/graph.py @@ -0,0 +1,281 @@ +"""Topological sort and related code""" + + +def block_sort(blocks, helper_blocks=None, calibration=None, return_io=False): + """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. + + Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's + inferred) that indicate their aggregate inputs and outputs + + Importantly, because including helper blocks in a blocks without additional measures + can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the + steady_state computation to resolve these cycles. + e.g. Consider Krusell Smith: + Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the helper block + because the "firm" block outputs "r", which the helper block takes as an input. + However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes + "K" as an input. + This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then + "firm" could be removed as a dependency of helper block and the cycle would be resolved. + + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + ignore_helpers: `bool` + A boolean indicating whether to account for/return the indices of helper blocks contained in blocks + Set to true when sorting for td and jac calculations + helper_indices: `list` + A list of indices corresponding to the helper blocks in the blocks + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using helper blocks. Read above docstring for more detail + return_io: `bool` + A boolean indicating whether to return the full set of input and output arguments from `blocks` + """ + # TODO: Decide whether we want to break out the input and output argument tracking and return to + # a different function... currently it's very convenient to slot it into block_sort directly, but it + # does clutter up the function body + if return_io: + # step 1: map outputs to blocks for topological sort + outmap, outargs = construct_output_map(blocks, helper_blocks=helper_blocks, + return_output_args=True) + + # step 2: dependency graph for topological sort and input list + dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, + helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep), inargs, outargs + else: + # step 1: map outputs to blocks for topological sort + outmap = construct_output_map(blocks, helper_blocks=helper_blocks) + + # step 2: dependency graph for topological sort and input list + dep = construct_dependency_graph(blocks, outmap, calibration=calibration, helper_blocks=helper_blocks) + + return topological_sort(dep) + + +def topological_sort(dep, names=None): + """Given directed graph pointing from each node to the nodes it depends on, topologically sort nodes""" + + # get complete set version of dep, and its reversal, and build initial stack of nodes with no dependencies + dep, revdep = complete_reverse_graph(dep) + nodeps = [n for n in dep if not dep[n]] + topsorted = [] + + # Kahn's algorithm: find something with no dependency, delete its edges and update + while nodeps: + n = nodeps.pop() + topsorted.append(n) + for n2 in revdep[n]: + dep[n2].remove(n) + if not dep[n2]: + nodeps.append(n2) + + # should be done: topsorted should be topologically sorted with same # of elements as original graphs! + if len(topsorted) != len(dep): + cycle_ints = find_cycle(dep, dep.keys() - set(topsorted)) + assert cycle_ints is not None, 'topological sort failed but no cycle, THIS SHOULD NEVER EVER HAPPEN' + cycle = [names[i] for i in cycle_ints] if names else cycle_ints + raise Exception(f'Topological sort failed: cyclic dependency {" -> ".join([str(n) for n in cycle])}') + + return topsorted + + +def construct_output_map(blocks, helper_blocks=None, return_output_args=False): + """Construct a map of outputs to the indices of the blocks that produce them. + + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + helper_blocks: `list` + A list of helper blocks, designed to aid steady state computation, to include in the sort + return_output_args: `bool` + A boolean indicating whether to track and return the full set of output arguments of all of the blocks + in `blocks` + """ + if helper_blocks is None: + helper_blocks = [] + + outmap = dict() + outargs = set() + for num, block in enumerate(blocks + helper_blocks): + # Find the relevant set of outputs corresponding to a block + if hasattr(block, "outputs"): + outputs = block.outputs + elif isinstance(block, dict): + outputs = block.keys() + else: + raise ValueError(f'{block} is not recognized as block or does not provide outputs') + + for o in outputs: + # Because some of the outputs of a helper block are, by construction, outputs that also appear in the + # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering + # throwing this ValueError + if o in outmap and block not in helper_blocks: + raise ValueError(f'{o} is output twice') + + # Priority sorting for standard blocks: + # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share + # a given output, such that the dependency graph is constructed on the standard blocks, where possible + if o not in outmap: + outmap[o] = num + if return_output_args: + outargs.add(o) + else: + continue + if return_output_args: + return outmap, outargs + else: + return outmap + + +def construct_dependency_graph(blocks, outmap, helper_blocks=None, + calibration=None, return_input_args=False): + """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where + this set is the set of blocks that the key block is dependent on. + + outmap is the output map (output to block index mapping) created by construct_output_map. + + See the docstring of block_sort for more details about the other arguments. + """ + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + + dep = {num: set() for num in range(len(blocks + helper_blocks))} + inargs = set() + for num, block in enumerate(blocks + helper_blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + if return_input_args: + inargs.add(i) + # Each potential input to a given block will either be 1) output by another block, + # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into + # the steady-state computation via the `calibration' dict. + # If the block is a helper block, then we want to check the calibration to see if the potential + # input is a pre-specified variable/parameter, and if it is then we will not add the block that + # produces that input as an output as a dependency. + # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic + # dependency, if it were not for this resolution. + if i in outmap and not (i in calibration and block in helper_blocks): + dep[num].add(outmap[i]) + if return_input_args: + return dep, inargs + else: + return dep + + +def find_outputs_that_are_intermediate_inputs(blocks, helper_blocks=None): + """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. + This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. + + See the docstring of construct_output_map for more details about the arguments. + """ + if helper_blocks is None: + helper_blocks = [] + + required = set() + outmap = construct_output_map(blocks, helper_blocks=helper_blocks) + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + if i in outmap: + required.add(i) + return required + + +def complete_reverse_graph(gph): + """Given directed graph represented as a dict from nodes to iterables of nodes, return representation of graph that + is complete (i.e. has each vertex pointing to some iterable, even if empty), and a complete version of reversed too. + Have returns be sets, for easy removal""" + + revgph = {n: set() for n in gph} + for n, e in gph.items(): + for n2 in e: + n2_edges = revgph.setdefault(n2, set()) + n2_edges.add(n) + + gph_missing_n = revgph.keys() - gph.keys() + gph = {**{k: set(v) for k, v in gph.items()}, **{n: set() for n in gph_missing_n}} + return gph, revgph + + +def find_cycle(dep, onlyset=None): + """Return list giving cycle if there is one, otherwise None""" + + # supposed to look only within 'onlyset', so filter out everything else + if onlyset is not None: + dep = {k: (set(v) & set(onlyset)) for k, v in dep.items() if k in onlyset} + + tovisit = set(dep.keys()) + stack = SetStack() + while tovisit or stack: + if stack: + # if stack has something, still need to proceed with DFS + n = stack.top() + if dep[n]: + # if there are any dependencies left, let's look at them + n2 = dep[n].pop() + if n2 in stack: + # we have a cycle, since this is already in our stack + i2loc = stack.index(n2) + return stack[i2loc:] + [stack[i2loc]] + else: + # no cycle, visit this node only if we haven't already visited it + if n2 in tovisit: + tovisit.remove(n2) + stack.add(n2) + else: + # if no dependencies left, then we're done with this node, so let's forget about it + stack.pop(n) + else: + # nothing left on stack, let's start the DFS from something new + n = tovisit.pop() + stack.add(n) + + # if we never find a cycle, we're done + return None + + +class SetStack: + """Stack implemented with list but tests membership with set to be efficient in big cases""" + + def __init__(self): + self.myset = set() + self.mylist = [] + + def add(self, x): + self.myset.add(x) + self.mylist.append(x) + + def pop(self): + x = self.mylist.pop() + self.myset.remove(x) + return x + + def top(self): + return self.mylist[-1] + + def index(self, x): + return self.mylist.index(x) + + def __contains__(self, x): + return x in self.myset + + def __len__(self): + return len(self.mylist) + + def __getitem__(self, i): + return self.mylist.__getitem__(i) + + def __repr__(self): + return self.mylist.__repr__() + + diff --git a/src/sequence_jacobian/utilities/interpolate.py b/src/sequence_jacobian/utilities/interpolate.py new file mode 100644 index 0000000..1a0ac5f --- /dev/null +++ b/src/sequence_jacobian/utilities/interpolate.py @@ -0,0 +1,185 @@ +"""Efficient linear interpolation exploiting monotonicity. + + Interpolates increasing query points xq against increasing data points x. + + - interpolate_y: (x, xq, y) -> yq + get interpolated values of yq at xq + + - interpolate_coord: (x, xq) -> (xqi, xqpi) + get representation xqi, xqpi of xq interpolated against x + xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] + + - apply_coord: (xqi, xqpi, y) -> yq + use representation xqi, xqpi to get yq at xq + yq = xqpi * y[xqi] + (1-xqpi) * y[xqi+1] + + Composing interpolate_coord and apply_coord gives interpolate_y. + + All three functions are written for vectors but can be broadcast to other dimensions + since we use Numba's guvectorize decorator. In these cases, interpolation is always + done on the final dimension. +""" + +import numpy as np +from numba import njit, guvectorize + + +@guvectorize(['void(float64[:], float64[:], float64[:], float64[:])'], '(n),(nq),(n)->(nq)') +def interpolate_y(x, xq, y, yq): + """Efficient linear interpolation exploiting monotonicity. + + Complexity O(n+nq), so most efficient when x and xq have comparable number of points. + Extrapolates linearly when xq out of domain of x. + + Parameters + ---------- + x : array (n), ascending data points + xq : array (nq), ascending query points + y : array (n), data points + + Returns + ---------- + yq : array (nq), interpolated points + """ + nxq, nx = xq.shape[0], x.shape[0] + + xi = 0 + x_low = x[0] + x_high = x[1] + for xqi_cur in range(nxq): + xq_cur = xq[xqi_cur] + while xi < nx - 2: + if x_high >= xq_cur: + break + xi += 1 + x_low = x_high + x_high = x[xi + 1] + + xqpi_cur = (x_high - xq_cur) / (x_high - x_low) + yq[xqi_cur] = xqpi_cur * y[xi] + (1 - xqpi_cur) * y[xi + 1] + + +@guvectorize(['void(float64[:], float64[:], uint32[:], float64[:])'], '(n),(nq)->(nq),(nq)') +def interpolate_coord(x, xq, xqi, xqpi): + """Get representation xqi, xqpi of xq interpolated against x: + xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] + + Parameters + ---------- + x : array (n), ascending data points + xq : array (nq), ascending query points + + Returns + ---------- + xqi : array (nq), indices of lower bracketing gridpoints + xqpi : array (nq), weights on lower bracketing gridpoints + """ + nxq, nx = xq.shape[0], x.shape[0] + + xi = 0 + x_low = x[0] + x_high = x[1] + for xqi_cur in range(nxq): + xq_cur = xq[xqi_cur] + while xi < nx - 2: + if x_high >= xq_cur: + break + xi += 1 + x_low = x_high + x_high = x[xi + 1] + + xqpi[xqi_cur] = (x_high - xq_cur) / (x_high - x_low) + xqi[xqi_cur] = xi + + +@guvectorize(['void(int64[:], float64[:], float64[:], float64[:])', + 'void(uint32[:], float64[:], float64[:], float64[:])'], '(nq),(nq),(n)->(nq)') +def apply_coord(x_i, x_pi, y, yq): + """Use representation xqi, xqpi to get yq at xq: + yq = xqpi * y[xqi] + (1-xqpi) * y[xqi+1] + + Parameters + ---------- + xqi : array (nq), indices of lower bracketing gridpoints + xqpi : array (nq), weights on lower bracketing gridpoints + y : array (n), data points + + Returns + ---------- + yq : array (nq), interpolated points + """ + nq = x_i.shape[0] + for iq in range(nq): + y_low = y[x_i[iq]] + y_high = y[x_i[iq]+1] + yq[iq] = x_pi[iq]*y_low + (1-x_pi[iq])*y_high + + +'''Part 2: More robust linear interpolation that does not require monotonicity in query points. + + Intended for general use in interpolating policy rules that we cannot be sure are monotonic. + Only get xqi, xqpi representation, for case where x is one-dimensional, in this application. +''' + + +def interpolate_coord_robust(x, xq, check_increasing=False): + """Linear interpolation exploiting monotonicity only in data x, not in query points xq. + Simple binary search, less efficient but more robust. + xq = xqpi * x[xqi] + (1-xqpi) * x[xqi+1] + + Main application intended to be universally-valid interpolation of policy rules. + Dimension k is optional. + + Parameters + ---------- + x : array (n), ascending data points + xq : array (k, nq), query points (in any order) + + Returns + ---------- + xqi : array (k, nq), indices of lower bracketing gridpoints + xqpi : array (k, nq), weights on lower bracketing gridpoints + """ + if x.ndim != 1: + raise ValueError('Data input to interpolate_coord_robust must have exactly one dimension') + + if check_increasing and np.any(x[:-1] >= x[1:]): + raise ValueError('Data input to interpolate_coord_robust must be strictly increasing') + + if xq.ndim == 1: + return interpolate_coord_robust_vector(x, xq) + else: + i, pi = interpolate_coord_robust_vector(x, xq.ravel()) + return i.reshape(xq.shape), pi.reshape(xq.shape) + + +@njit +def interpolate_coord_robust_vector(x, xq): + """Does interpolate_coord_robust where xq must be a vector, more general function is wrapper""" + + n = len(x) + nq = len(xq) + xqi = np.empty(nq, dtype=np.uint32) + xqpi = np.empty(nq) + + for iq in range(nq): + if xq[iq] < x[0]: + ilow = 0 + elif xq[iq] > x[-2]: + ilow = n-2 + else: + # start binary search + # should end with ilow and ihigh exactly 1 apart, bracketing variable + ihigh = n-1 + ilow = 0 + while ihigh - ilow > 1: + imid = (ihigh + ilow) // 2 + if xq[iq] > x[imid]: + ilow = imid + else: + ihigh = imid + + xqi[iq] = ilow + xqpi[iq] = (x[ilow+1] - xq[iq]) / (x[ilow+1] - x[ilow]) + + return xqi, xqpi diff --git a/src/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py new file mode 100644 index 0000000..cf47e7b --- /dev/null +++ b/src/sequence_jacobian/utilities/misc.py @@ -0,0 +1,175 @@ +"""Assorted other utilities""" + +import numpy as np +import scipy.linalg +import re +import inspect +import warnings +from ..jacobian.classes import JacobianDict + + +def make_tuple(x): + """If not tuple or list, make into tuple with one element. + + Wrapping with this allows user to write, e.g.: + "return r" rather than "return (r,)" + "policy='a'" rather than "policy=('a',)" + """ + return (x,) if not (isinstance(x, tuple) or isinstance(x, list)) else x + + +def input_list(f): + """Return list of function inputs (both positional and keyword arguments)""" + return list(inspect.signature(f).parameters) + + +def input_arg_list(f): + """Return list of function positional arguments *only*""" + arg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default == p.empty: + arg_list.append(p.name) + return arg_list + + +def input_kwarg_list(f): + """Return list of function keyword arguments *only*""" + kwarg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default != p.empty: + kwarg_list.append(p.name) + return kwarg_list + + +def output_list(f): + """Scans source code of function to detect statement like + + 'return L, Div' + + and reports the list ['L', 'Div']. + + Important to write functions in this way when they will be scanned by output_list, for + either SimpleBlock or HetBlock. + """ + return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') + + +def numeric_primitive(instance): + # If it is already a primitive, just return it + if type(instance) in {int, float}: + return instance + elif isinstance(instance, np.ndarray): + if np.issubdtype(instance.dtype, np.number): + return np.array(instance) + else: + raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance.dtype}," + f" which is not a valid numeric type.") + elif type(instance) in {tuple, list}: + instance_array = np.asarray(instance) + if np.issubdtype(instance_array.dtype, np.number): + return type(instance)(instance_array) + else: + raise ValueError(f"The tuple/list argument provided to numeric_primitive has dtype: {instance_array.dtype}," + f" which is not a valid numeric type.") + else: + return instance.real if np.isscalar(instance) else instance.base + + +def demean(x): + return x - x.sum()/x.size + + +# simpler aliases for LU factorization and solution +def factor(X): + return scipy.linalg.lu_factor(X) + + +def factored_solve(Z, y): + return scipy.linalg.lu_solve(Z, y) + + +# The below functions are used in steady_state +def unprime(s): + """Given a variable's name as a `str`, check if the variable is a prime, i.e. has "_p" at the end. + If so, return the unprimed version, if not return itself.""" + if s[-2:] == "_p": + return s[:-2] + else: + return s + + +def uncapitalize(s): + return s[0].lower() + s[1:] + + +def list_diff(l1, l2): + """Returns the list that is the "set difference" between l1 and l2 (based on element values)""" + o_list = [] + for k in set(l1) - set(l2): + o_list.append(k) + return o_list + + +def dict_diff(d1, d2): + """Returns the dictionary that is the "set difference" between d1 and d2 (based on keys, not key-value pairs) + E.g. d1 = {"a": 1, "b": 2}, d2 = {"b": 5}, then dict_diff(d1, d2) = {"a": 1} + """ + o_dict = {} + for k in set(d1.keys()) - set(d2.keys()): + o_dict[k] = d1[k] + return o_dict + + +def smart_set(data): + # We want set to construct a single-element set for strings, i.e. ignoring the .iter method of strings + if isinstance(data, str): + return {data} + else: + return set(data) + + +def smart_zip(keys, values): + """For handling the case where keys and values may be scalars""" + if isinstance(values, float): + return zip(keys, [values]) + else: + return zip(keys, values) + + +def smart_zeros(n): + """Return either the float 0. or a np.ndarray of length 0 depending on whether n > 1""" + if n > 1: + return np.zeros(n) + else: + return 0. + + +def verify_saved_jacobian(block_name, Js, outputs, inputs, T): + """Verify that pre-computed Jacobian has all the right outputs, inputs, and length.""" + if block_name not in Js.keys(): + # don't throw warning, this will happen often for simple blocks + return False + J = Js[block_name] + + if not isinstance(J, JacobianDict): + warnings.warn(f'Js[{block_name}] is not a JacobianDict.') + return False + + if not set(outputs).issubset(set(J.outputs)): + missing = set(outputs).difference(set(J.outputs)) + warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') + return False + + if not set(inputs).issubset(set(J.inputs)): + missing = set(inputs).difference(set(J.inputs)) + warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') + return False + + # Jacobian of simple blocks may have a sparse representation + if T is not None: + Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] + if T != Tsaved: + warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') + return False + + return True diff --git a/src/sequence_jacobian/utilities/optimized_routines.py b/src/sequence_jacobian/utilities/optimized_routines.py new file mode 100644 index 0000000..94f1724 --- /dev/null +++ b/src/sequence_jacobian/utilities/optimized_routines.py @@ -0,0 +1,45 @@ +"""Njitted routines to speed up some steps in backward iteration or aggregation""" + +import numpy as np +from numba import njit + + +@njit +def setmin(x, xmin): + """Set 2-dimensional array x where each row is ascending equal to equal to max(x, xmin).""" + ni, nj = x.shape + for i in range(ni): + for j in range(nj): + if x[i, j] < xmin: + x[i, j] = xmin + else: + break + + +@njit +def within_tolerance(x1, x2, tol): + """Efficiently test max(abs(x1-x2)) <= tol for arrays of same dimensions x1, x2.""" + y1 = x1.ravel() + y2 = x2.ravel() + + for i in range(y1.shape[0]): + if np.abs(y1[i] - y2[i]) > tol: + return False + return True + + +@njit +def fast_aggregate(X, Y): + """If X has dims (T, ...) and Y has dims (T, ...), do dot product for each T to get length-T vector. + + Identical to np.sum(X*Y, axis=(1,...,X.ndim-1)) but avoids costly creation of intermediates, useful + for speeding up aggregation in td by factor of 4 to 5.""" + T = X.shape[0] + Xnew = X.reshape(T, -1) + Ynew = Y.reshape(T, -1) + Z = np.empty(T) + for t in range(T): + Z[t] = Xnew[t, :] @ Ynew[t, :] + return Z + + diff --git a/src/sequence_jacobian/utilities/solvers.py b/src/sequence_jacobian/utilities/solvers.py new file mode 100644 index 0000000..076a809 --- /dev/null +++ b/src/sequence_jacobian/utilities/solvers.py @@ -0,0 +1,148 @@ +"""Simple nonlinear solvers""" + +import numpy as np +import warnings + + +def newton_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verbose=True): + """Simple line search solver for root x satisfying f(x)=0 using Newton direction. + + Backtracks if input invalid or improvement is not at least half the predicted improvement. + + Parameters + ---------- + f : function, to solve for f(x)=0, input and output are arrays of same length + x0 : array (n), initial guess for x + y0 : [optional] array (n), y0=f(x0), if already known + tol : [optional] scalar, solver exits successfully when |f(x)| < tol + maxcount : [optional] int, maximum number of Newton steps + backtrack_c : [optional] scalar, fraction to backtrack if step unsuccessful, i.e. + if we tried step from x to x+dx, now try x+backtrack_c*dx + + Returns + ---------- + x : array (n), (approximate) root of f(x)=0 + y : array (n), y=f(x), satisfies |y| < tol + """ + + x, y = x0, y0 + if y is None: + y = f(x) + + for count in range(maxcount): + if verbose: + printit(count, x, y) + + if np.max(np.abs(y)) < tol: + return x, y + + J = obtain_J(f, x, y) + dx = np.linalg.solve(J, -y) + + # backtrack at most 29 times + for bcount in range(30): + try: + ynew = f(x + dx) + except ValueError: + if verbose: + print('backtracking\n') + dx *= backtrack_c + else: + predicted_improvement = -np.sum((J @ dx) * y) * ((1 - 1 / 2 ** bcount) + 1) / 2 + actual_improvement = (np.sum(y ** 2) - np.sum(ynew ** 2)) / 2 + if actual_improvement < predicted_improvement / 2: + if verbose: + print('backtracking\n') + dx *= backtrack_c + else: + y = ynew + x += dx + break + else: + raise ValueError('Too many backtracks, maybe bad initial guess?') + else: + raise ValueError(f'No convergence after {maxcount} iterations') + + +def broyden_solver(f, x0, y0=None, tol=1E-9, maxcount=100, backtrack_c=0.5, verbose=True): + """Similar to newton_solver, but solves f(x)=0 using approximate rather than exact Newton direction, + obtaining approximate Jacobian J=f'(x) from Broyden updating (starting from exact Newton at f'(x0)). + + Backtracks only if error raised by evaluation of f, since improvement criterion no longer guaranteed + to work for any amount of backtracking if Jacobian not exact. + """ + + x, y = x0, y0 + if y is None: + y = f(x) + + for count in range(maxcount): + if verbose: + printit(count, x, y) + + if np.max(np.abs(y)) < tol: + return x, y + + # initialize J with Newton! + if count == 0: + J = obtain_J(f, x, y) + + if len(x) == len(y): + dx = np.linalg.solve(J, -y) + elif len(x) < len(y): + warnings.warn(f"Dimension of x, {len(x)} is less than dimension of y, {len(y)}." + f" Using least-squares criterion to solve for approximate root.") + dx = np.linalg.lstsq(J, -y, rcond=None)[0] + else: + raise ValueError(f"Dimension of x, {len(x)} is greater than dimension of y, {len(y)}." + f" Cannot solve underdetermined system.") + + # backtrack at most 29 times + for bcount in range(30): + # note: can't test for improvement with Broyden because maybe + # the function doesn't improve locally in this direction, since + # J isn't the exact Jacobian + try: + ynew = f(x + dx) + except ValueError: + if verbose: + print('backtracking\n') + dx *= backtrack_c + else: + J = broyden_update(J, dx, ynew - y) + y = ynew + x += dx + break + else: + raise ValueError('Too many backtracks, maybe bad initial guess?') + else: + raise ValueError(f'No convergence after {maxcount} iterations') + + +def obtain_J(f, x, y, h=1E-5): + """Finds Jacobian f'(x) around y=f(x)""" + nx = x.shape[0] + ny = y.shape[0] + J = np.empty((ny, nx)) + + for i in range(nx): + dx = h * (np.arange(nx) == i) + J[:, i] = (f(x + dx) - y) / h + return J + + +def broyden_update(J, dx, dy): + """Returns Broyden update to approximate Jacobian J, given that last change in inputs to function + was dx and led to output change of dy.""" + return J + np.outer(((dy - J @ dx) / np.linalg.norm(dx) ** 2), dx) + + +def printit(it, x, y, **kwargs): + """Convenience printing function for verbose iterations""" + print(f'On iteration {it}') + print(('x = %.3f' + ',%.3f' * (len(x) - 1)) % tuple(x)) + print(('y = %.3f' + ',%.3f' * (len(y) - 1)) % tuple(y)) + for kw, val in kwargs.items(): + print(f'{kw} = {val:.3f}') + print('\n') + diff --git a/src/sequence_jacobian/visualization/__init__.py b/src/sequence_jacobian/visualization/__init__.py new file mode 100644 index 0000000..2850057 --- /dev/null +++ b/src/sequence_jacobian/visualization/__init__.py @@ -0,0 +1 @@ +"""Various tools for plotting and creating visualizations""" diff --git a/src/sequence_jacobian/visualization/draw_dag.py b/src/sequence_jacobian/visualization/draw_dag.py new file mode 100644 index 0000000..9b75ca5 --- /dev/null +++ b/src/sequence_jacobian/visualization/draw_dag.py @@ -0,0 +1,161 @@ +"""Provides the functionality for basic DAG visualization""" + +import warnings +from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph + + +# Implement DAG drawing functions as "soft" dependencies to not enforce the installation of graphviz, since +# it's not required for the rest of the sequence-jacobian code to run +try: + """ + DAG Graph routine + Requires installing graphviz package and executables + https://www.graphviz.org/ + + On a mac this can be done as follows: + 1) Download macports at: + https://www.macports.org/install.php + 2) On the command line, install graphviz with macports by typing + sudo port install graphviz + + """ + from graphviz import Digraph + + + def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_helpers=True, calibration=None, + showdag=False, leftright=False, filename='modeldag'): + """ + Visualizes a Directed Acyclic Graph (DAG) of a set of blocks, exogenous variables, unknowns, and targets + + block_list: `list` + Blocks to be represented as nodes within a DAG + exogenous: `list` (optional) + Exogenous variables, to be represented on DAG + unknowns: `list` (optional) + Unknown variables, to be represented on DAG + targets: `list` (optional) + Target variables, to be represented on DAG + ignore_helpers: `bool` + A boolean indicating whether to also draw HelperBlocks contained in block_list into the DAG + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using HelperBlocks. Read `block_sort` docstring for more detail + showdag: `bool` + If True, export and plot pdf file. If false, export png file and do not plot + leftright: `bool` + If True, plots DAG from left to right instead of top to bottom + + return: None + """ + + # To prevent having mutable variables as keyword arguments + exogenous = [] if exogenous is None else exogenous + unknowns = [] if unknowns is None else unknowns + targets = [] if targets is None else targets + + # obtain the topological sort + topsorted = block_sort(block_list, ignore_helpers=ignore_helpers, calibration=calibration) + # get sorted list of blocks + block_list_sorted = [block_list[i] for i in topsorted] + # Obtain the dependency list of the sorted set of blocks + dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted), + ignore_helpers=ignore_helpers, calibration=calibration) + + # Draw DAG + dot = Digraph(comment='Model DAG') + + # Make left-to-right + if leftright: + dot.attr(rankdir='LR', ratio='compress', center='true') + else: + dot.attr(ratio='auto', center='true') + + # add initial nodes (one for exogenous, one for unknowns) provided those are not empty lists + if exogenous: + dot.node('exog', 'exogenous', shape='box') + if unknowns: + dot.node('unknowns', 'unknowns', shape='box') + if targets: + dot.node('targets', 'targets', shape='diamond') + + # add nodes sequentially in order + for i in dep_list_sorted: + if hasattr(block_list_sorted[i], 'hetinput'): + # HA block + dot.node(str(i), 'HA [' + str(i) + ']') + elif hasattr(block_list_sorted[i], 'block_list'): + # Solved block + dot.node(str(i), block_list_sorted[i].block_list[0].f.__name__ + '[solved,' + str(i) + ']') + else: + # Simple block + dot.node(str(i), block_list_sorted[i].f.__name__ + ' [' + str(i) + ']') + + # nodes from exogenous to i (figure out if needed and draw) + if exogenous: + edgelabel = block_list_sorted[i].inputs & set(exogenous) + if len(edgelabel) != 0: + edgelabel_list = list(edgelabel) + edgelabel_str = ', '.join(str(e) for e in edgelabel_list) + dot.edge('exog', str(i), label=str(edgelabel_str)) + + # nodes from unknowns to i (figure out if needed and draw) + if unknowns: + edgelabel = block_list_sorted[i].inputs & set(unknowns) + if len(edgelabel) != 0: + edgelabel_list = list(edgelabel) + edgelabel_str = ', '.join(str(e) for e in edgelabel_list) + dot.edge('unknowns', str(i), label=str(edgelabel_str)) + + # nodes from i to final targets + for target in targets: + if target in block_list_sorted[i].outputs: + dot.edge(str(i), 'targets', label=target) + + # nodes from any interior block to i + for j in dep_list_sorted[i]: + # figure out inputs of i that are also outputs of j + edgelabel = block_list_sorted[i].inputs & block_list_sorted[j].outputs + edgelabel_list = list(edgelabel) + edgelabel_str = ', '.join(str(e) for e in edgelabel_list) + + # draw edge from j to i + dot.edge(str(j), str(i), label=str(edgelabel_str)) + + if showdag: + dot.render('dagexport/' + filename, view=True, cleanup=True) + else: + dot.render('dagexport/' + filename, format='png', cleanup=True) + # print(dot.source) + + + def draw_solved(solvedblock, filename='solveddag'): + # Inspects a solved block by drawing its DAG + draw_dag([solvedblock.block_list[0]], unknowns=solvedblock.unknowns, targets=solvedblock.targets, + filename=filename, showdag=True) + + + def inspect_solved(block_list): + # Inspects all the solved blocks by running through each and drawing its DAG in turn + for block in block_list: + if hasattr(block, 'block_list'): + draw_solved(block, filename=str(block.block_list[0].f.__name__)) +except ImportError: + def draw_dag(*args, **kwargs): + warnings.warn("\nAttempted to use `draw_dag` when the package `graphviz` has not yet been installed. \n" + "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" + "Once installed, re-load sequence-jacobian to produce DAG figures.") + pass + + + def draw_solved(*args, **kwargs): + warnings.warn("\nAttempted to use `draw_solved` when the package `graphviz` has not yet been installed. \n" + "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" + "Once installed, re-load sequence-jacobian to produce DAG figures.") + pass + + + def inspect_solved(*args, **kwargs): + warnings.warn("\nAttempted to use `inspect_solved` when the package `graphviz` has not yet been installed. \n" + "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" + "Once installed, re-load sequence-jacobian to produce DAG figures.") + pass From e42f73048c5d29a250b7b7c8a45ec66287a72e0c Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 24 May 2021 16:44:49 -0500 Subject: [PATCH 155/288] Allow subtracting ImpulseDicts. --- src/sequence_jacobian/blocks/support/impulse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index 0d29362..4be8f93 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -46,7 +46,7 @@ def __add__(self, other): def __sub__(self, other): if isinstance(other, (float, int)): return type(self)({k: v - other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): + if isinstance(other, (SteadyStateDict, ImpulseDict)): return type(self)({k: v - other[k] for k, v in self.impulse.items()}) def __mul__(self, other): From 66cbe8dec36675b399b83b243da3f0b3535d1c3e Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 25 May 2021 09:43:45 -0500 Subject: [PATCH 156/288] Untrack egg-info folder. --- .gitignore | 1 + src/sequence_jacobian.egg-info/PKG-INFO | 69 ------------------- src/sequence_jacobian.egg-info/SOURCES.txt | 7 -- .../dependency_links.txt | 1 - src/sequence_jacobian.egg-info/requires.txt | 4 -- src/sequence_jacobian.egg-info/top_level.txt | 1 - 6 files changed, 1 insertion(+), 82 deletions(-) delete mode 100644 src/sequence_jacobian.egg-info/PKG-INFO delete mode 100644 src/sequence_jacobian.egg-info/SOURCES.txt delete mode 100644 src/sequence_jacobian.egg-info/dependency_links.txt delete mode 100644 src/sequence_jacobian.egg-info/requires.txt delete mode 100644 src/sequence_jacobian.egg-info/top_level.txt diff --git a/.gitignore b/.gitignore index b102664..f6ec28d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ scripts/ wiki/ +*.egg-info/ diff --git a/src/sequence_jacobian.egg-info/PKG-INFO b/src/sequence_jacobian.egg-info/PKG-INFO deleted file mode 100644 index 535e1d1..0000000 --- a/src/sequence_jacobian.egg-info/PKG-INFO +++ /dev/null @@ -1,69 +0,0 @@ -Metadata-Version: 2.1 -Name: sequence-jacobian -Version: 0.0.1 -Summary: Sequence-Space Jacobian Methods for Solving and Estimating Heterogeneous Agent Models -Home-page: https://github.com/shade-econ/sequence-jacobian -Author: Michael Cai -Author-email: michaelcai@u.northwestern.edu -License: UNKNOWN -Description: # Sequence-Space Jacobian - - Interactive guide to Auclert, Bardóczy, Rognlie, Straub (2019): - "Using the Sequence-Space Jacobian to Solve and Estimate Heterogeneous-Agent Models". - - [Click here](https://github.com/shade-econ/sequence-jacobian/archive/master.zip) to download all files as a zip. Note: **major update** on July 26, 2019. - - ## 1. Resources - - - [Paper](https://shade-econ.github.io/sequence-jacobian/sequence_jacobian_paper.pdf) - - [Beamer slides](https://shade-econ.github.io/sequence-jacobian/sequence_jacobian_slides.pdf) - - RBC notebook ([html](https://shade-econ.github.io/sequence-jacobian/rbc.html)) ([Jupyter](notebooks/rbc.ipynb)) - - Krusell-Smith notebook ([html](https://shade-econ.github.io/sequence-jacobian/krusell_smith.html)) ([Jupyter](notebooks/krusell_smith.ipynb)) - - One-asset HANK notebook ([html](https://shade-econ.github.io/sequence-jacobian/hank.html)) ([Jupyter](notebooks/hank.ipynb)) - - Two-asset HANK notebook ([html](https://shade-econ.github.io/sequence-jacobian/two_asset.html)) ([Jupyter](notebooks/two_asset.ipynb)) - - HA Jacobian notebook ([html](https://shade-econ.github.io/sequence-jacobian/het_jacobian.html)) ([Jupyter](notebooks/het_jacobian.ipynb)) - - ### 1.1 RBC notebook - - **Warm-up.** Get familiar with solving models in sequence space using our tools. If you don't have Python, - just start by reading the `html` version. If you do, we recommend downloading our code and running the Jupyter notebook directly on your computer. - - ### 1.2. Krusell-Smith notebook - - **The first example.** A comprehensive tutorial in the context of a simple, well-known HA model. Shows how to compute the Jacobian both "by hand" and with our automated tools. Also shows how to calculate second moments and the likelihood function. - - ### 1.3. One-asset HANK notebook - - **The second example.** Generalizes to a more complex model, with a focus on our automated tools to streamline the workflow. Introduces our winding number criterion for local determinacy. - - ### 1.4. Two-asset HANK notebook - - **The third example.** Showcases the workflow for solving a state-of-the-art HANK model where households hold liquid and illiquid assets, and there are sticky prices, sticky wages, and capital adjustment costs on the production side. Introduces the concept of solved blocks. - - ### 1.5. HA Jacobian notebook - - **Inside the black box.** A step-by-step examination of our fake news algorithm to compute Jacobians of HA blocks. - - ## 2. Setting up Python - - To install a full distribution of Python, with all of the packages and tools you will need to run our code, - download the latest [Python 3 Anaconda](https://www.anaconda.com/distribution/) distribution. - **Note:** make sure you choose the installer for Python version 3. - Once you install Anaconda, you will be able to play with the notebooks we provided. Just open a terminal, change - directory to the folder with notebooks, and type `jupyter notebook`. This will launch the notebook dashboard in your - default browser. Click on a notebook to get started. - - For more information on Jupyter notebooks, check out the - [official quick start guide](https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/). - If you'd like to learn more about Python, the [QuantEcon](https://lectures.quantecon.org/py/) lectures of - Tom Sargent and John Stachurski are a great place to start. - - - - -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Requires-Python: >=3.7 -Description-Content-Type: text/markdown diff --git a/src/sequence_jacobian.egg-info/SOURCES.txt b/src/sequence_jacobian.egg-info/SOURCES.txt deleted file mode 100644 index 98ef412..0000000 --- a/src/sequence_jacobian.egg-info/SOURCES.txt +++ /dev/null @@ -1,7 +0,0 @@ -README.md -setup.py -src/sequence_jacobian.egg-info/PKG-INFO -src/sequence_jacobian.egg-info/SOURCES.txt -src/sequence_jacobian.egg-info/dependency_links.txt -src/sequence_jacobian.egg-info/requires.txt -src/sequence_jacobian.egg-info/top_level.txt \ No newline at end of file diff --git a/src/sequence_jacobian.egg-info/dependency_links.txt b/src/sequence_jacobian.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/sequence_jacobian.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/sequence_jacobian.egg-info/requires.txt b/src/sequence_jacobian.egg-info/requires.txt deleted file mode 100644 index 1f49b43..0000000 --- a/src/sequence_jacobian.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -numpy>=1.18 -scipy>=1.5 -numba>=0.50 -xarray>=0.17 diff --git a/src/sequence_jacobian.egg-info/top_level.txt b/src/sequence_jacobian.egg-info/top_level.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/sequence_jacobian.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ - From 70306e6608f69e7bdbb96262a6829ff085167c58 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 8 Jun 2021 09:47:01 -0500 Subject: [PATCH 157/288] Created copy of het_block. --- src/sequence_jacobian/blocks/hetdc_block.py | 922 ++++++++++++++++++++ 1 file changed, 922 insertions(+) create mode 100644 src/sequence_jacobian/blocks/hetdc_block.py diff --git a/src/sequence_jacobian/blocks/hetdc_block.py b/src/sequence_jacobian/blocks/hetdc_block.py new file mode 100644 index 0000000..259d4d8 --- /dev/null +++ b/src/sequence_jacobian/blocks/hetdc_block.py @@ -0,0 +1,922 @@ +import warnings +import copy +import numpy as np + +from .support.impulse import ImpulseDict +from ..primitives import Block +from .. import utilities as utils +from ..steady_state.classes import SteadyStateDict +from ..jacobian.classes import JacobianDict +from ..devtools.deprecate import rename_output_list_to_outputs +from ..utilities.misc import verify_saved_jacobian + + +def het(exogenous, policy, backward, backward_init=None): + def decorator(back_step_fun): + return HetBlock(back_step_fun, exogenous, policy, backward, backward_init=backward_init) + return decorator + + +class HetBlock(Block): + """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. + + IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since + the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle + aggregation of non-aggregate outputs across the distribution and return aggregates as upper-case equivalents + of the `policy` and non-aggregate output variables specified in the backward step function. + """ + + def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None): + """Construct HetBlock from backward iteration function. + + Parameters + ---------- + back_step_fun : function + backward iteration function + exogenous : str + name of Markov transition matrix for exogenous variable + (now only single allowed for simplicity; use Kronecker product for more) + policy : str or sequence of str + names of policy variables of endogenous, continuous state variables + e.g. assets 'a', must be returned by function + backward : str or sequence of str + variables that together comprise the 'v' that we use for iterating backward + must appear both as outputs and as arguments + + It is assumed that every output of the function (except possibly backward), including policy, + will be on a grid of dimension 1 + len(policy), where the first dimension is the exogenous + variable and then the remaining dimensions are each of the continuous policy variables, in the + same order they are listed in 'policy'. + + The Markov transition matrix between the current and future period and backward iteration + variables should appear in the backward iteration function with '_p' subscripts ("prime") to + indicate that they come from the next period. + + Currently, we only support up to two policy variables. + """ + self.name = back_step_fun.__name__ + + # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. + # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) + self.back_step_fun = back_step_fun + + # self.back_step_outputs and self.back_step_inputs are all of the output and input arguments of + # self.back_step_fun, the variables used in the backward iteration, + # which generally include value and/or policy functions. + self.back_step_output_list = utils.misc.output_list(back_step_fun) + self.back_step_outputs = set(self.back_step_output_list) + self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) + + # See the docstring of HetBlock for details on the attributes directly below + self.exogenous = exogenous + self.policy, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (policy, backward)) + + # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" + # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun + # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to + # the primed key name, such that self.back_step_fun will properly call those variables. + # e.g. the key "Va" will become "Va_p", associated to the same value. + self.inputs_to_be_primed = {self.exogenous} | set(self.back_iter_vars) + + # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward + # iteration variables themselves. + self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) + + # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used + # in utils.graph.block_sort to topologically sort blocks along the DAG + # according to their aggregate outputs and inputs. + self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} + self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} + self.inputs.remove(exogenous + '_p') + self.inputs.add(exogenous) + + # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. + # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. + self.hetinput = None + self.hetinput_inputs = set() + self.hetinput_outputs = set() + self.hetinput_outputs_order = tuple() + + # start without a hetoutput + self.hetoutput = None + self.hetoutput_inputs = set() + self.hetoutput_outputs = set() + self.hetoutput_outputs_order = tuple() + + # The set of variables that will be wrapped in a separate namespace for this HetBlock + # as opposed to being available at the top level + self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} + + if len(self.policy) > 2: + raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") + + # Checking that the various inputs/outputs attributes are correctly set + if self.exogenous + '_p' not in self.back_step_inputs: + raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") + + for pol in self.policy: + if pol not in self.back_step_outputs: + raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") + if pol[0].isupper(): + raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") + + for back in self.back_iter_vars: + if back + '_p' not in self.back_step_inputs: + raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") + + if back not in self.back_step_outputs: + raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") + + for out in self.non_back_iter_outputs: + if out[0].isupper(): + raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") + + # Add the backward iteration initializer function (the initial guesses for self.back_iter_vars) + if backward_init is None: + # TODO: Think about implementing some "automated way" of providing + # an initial guess for the backward iteration. + self.backward_init = backward_init + else: + self.backward_init = backward_init + + # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. + + def __repr__(self): + """Nice string representation of HetBlock for printing to console""" + if self.hetinput is not None: + if self.hetoutput is not None: + return f"" + else: + return f"" + else: + return f"" + + '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts + - ss : do backward and forward iteration until convergence to get complete steady state + - td : do backward and forward iteration up to T to compute dynamics given some shocks + - jac : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm + - ajac : compute asymptotic columns of jacobians output by jac, also using fake news algorithm + + - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput + - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after + each backward iteration step in td + ''' + + # TODO: Deprecated methods, to be removed! + def ss(self, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) + return self.steady_state(kwargs) + + def td(self, ss, **kwargs): + warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", + DeprecationWarning) + return self.impulse_nonlinear(ss, **kwargs) + + def jac(self, ss, shock_list=None, T=None, **kwargs): + if shock_list is None: + shock_list = list(self.inputs) + warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" + "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", + DeprecationWarning) + return self.jacobian(ss, shock_list, T, **kwargs) + + def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, + forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): + """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. + + Parameters + ---------- + backward_tol : [optional] float + in backward iteration, max abs diff between policy in consecutive steps needed for convergence + backward_maxit : [optional] int + maximum number of backward iterations, if 'backward_tol' not reached by then, raise error + forward_tol : [optional] float + in forward iteration, max abs diff between dist in consecutive steps needed for convergence + forward_maxit : [optional] int + maximum number of forward iterations, if 'forward_tol' not reached by then, raise error + + kwargs : dict + The following inputs are required as keyword arguments, which show up in 'kwargs': + - The exogenous Markov matrix, e.g. Pi=... if self.exogenous=='Pi' + - A seed for each backward variable, e.g. Va=... and Vb=... if self.back_iter_vars==('Va','Vb') + - A grid for each policy variable, e.g. a_grid=... and b_grid=... if self.policy==('a','b') + - All other inputs to the backward iteration function self.back_step_fun, except _p added to + for self.exogenous and self.back_iter_vars, for which the method uses steady-state values. + If there is a self.hetinput, then we need the inputs to that, not to self.back_step_fun. + + Other inputs in 'kwargs' are optional: + - A seed for the distribution: D=... + - If no seed for the distribution is provided, a seed for the invariant distribution + of the Markov process, e.g. Pi_seed=... if self.exogenous=='Pi' + + Returns + ---------- + ss : dict, contains + - ss inputs of self.back_step_fun and (if present) self.hetinput + - ss outputs of self.back_step_fun + - ss distribution 'D' + - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars + """ + + ss = copy.deepcopy(calibration) + + # extract information from calibration + Pi = calibration[self.exogenous] + grid = {k: calibration[k+'_grid'] for k in self.policy} + D_seed = calibration.get('D', None) + pi_seed = calibration.get(self.exogenous + '_seed', None) + + # run backward iteration + sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) + ss.update(sspol) + + # run forward iteration + D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) + ss.update({"D": D}) + + # aggregate all outputs other than backward variables on grid, capitalize + aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} + ss.update(aggregates) + + if hetoutput and self.hetoutput is not None: + hetoutputs = self.hetoutput.evaluate(ss) + aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") + else: + hetoutputs = {} + aggregate_hetoutputs = {} + ss.update({**hetoutputs, **aggregate_hetoutputs}) + + return SteadyStateDict(ss, internal=self) + + def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): + """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, + assuming that we start and end in steady state ss, and that all inputs not specified in + kwargs are constant at their ss values. Analog to SimpleBlock.td. + + CANNOT provide time-varying paths of grid or Markov transition matrix for now. + + Parameters + ---------- + ss : SteadyStateDict + all steady-state info, intended to be from .ss() + exogenous : dict of {str : array(T, ...)} + all time-varying inputs here (in deviations), with first dimension being time + this must have same length T for all entries (all outputs will be calculated up to T) + monotonic : [optional] bool + flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us + to use faster interpolation routines, otherwise use slower robust to nonmonotonicity + returnindividual : [optional] bool + return distribution and full outputs on grid + grid_paths: [optional] dict of {str: array(T, Number of grid points)} + time-varying grids for policies + + Returns + ---------- + td : dict + if returnindividual = False, time paths for aggregates (uppercase) for all outputs + of self.back_step_fun except self.back_iter_vars + if returnindividual = True, additionally time paths for distribution and for all outputs + of self.back_Step_fun on the full grid + """ + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + + # copy from ss info + Pi_T = ss[self.exogenous].T.copy() + D = ss.internal[self.name]['D'] + + # construct grids for policy variables either from the steady state grid if the grid is meant to be + # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. + grid = {} + use_ss_grid = {} + for k in self.policy: + if grid_paths is not None and k in grid_paths: + grid[k] = grid_paths[k] + use_ss_grid[k] = False + else: + grid[k] = ss[k+"_grid"] + use_ss_grid[k] = True + + # allocate empty arrays to store result, assume all like D + individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} + hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} + + # backward iteration + backdict = dict(ss.items()) + backdict.update(copy.deepcopy(ss.internal[self.name])) + for t in reversed(range(T)): + # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! + backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + individual = {k: v for k, v in zip(self.back_step_output_list, + self.back_step_fun(**self.make_inputs(backdict)))} + backdict.update({k: individual[k] for k in self.back_iter_vars}) + + if self.hetoutput is not None: + hetoutput = self.hetoutput.evaluate(backdict) + for k in self.hetoutput_outputs: + hetoutput_paths[k][t, ...] = hetoutput[k] + + for k in self.non_back_iter_outputs: + individual_paths[k][t, ...] = individual[k] + + D_path = np.empty((T,) + D.shape) + D_path[0, ...] = D + for t in range(T-1): + # have to interpolate policy separately for each t to get sparse transition matrices + sspol_i = {} + sspol_pi = {} + for pol in self.policy: + if use_ss_grid[pol]: + grid_var = grid[pol] + else: + grid_var = grid[pol][t, ...] + if monotonic: + # TODO: change for two-asset case so assumption is monotonicity in own asset, not anything else + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord(grid_var, + individual_paths[pol][t, ...]) + else: + sspol_i[pol], sspol_pi[pol] =\ + utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[pol][t, ...]) + + # step forward + D_path[t+1, ...] = self.forward_step(D_path[t, ...], Pi_T, sspol_i, sspol_pi) + + # obtain aggregates of all outputs, made uppercase + aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) + for o in self.non_back_iter_outputs} + if self.hetoutput: + aggregate_hetoutputs = self.hetoutput.aggregate(hetoutput_paths, D_path, backdict, mode="td") + else: + aggregate_hetoutputs = {} + + # return either this, or also include distributional information + if returnindividual: + return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, + 'D': D_path}) - ss + else: + return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss + + def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): + # infer T from exogenous, check that all shocks have same length + shock_lengths = [x.shape[0] for x in exogenous.values()] + if shock_lengths[1:] != shock_lengths[:-1]: + raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + T = shock_lengths[0] + + return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) + + def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): + """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. + + Parameters + ---------- + ss : dict, + all steady-state info, intended to be from .ss() + T : [optional] int + number of time periods for T*T Jacobian + exogenous : list of str + names of input variables to differentiate wrt (main cost scales with # of inputs) + outputs : list of str + names of output variables to get derivatives of, if not provided assume all outputs of + self.back_step_fun except self.back_iter_vars + h : [optional] float + h for numerical differentiation of backward iteration + Js : [optional] dict of {str: JacobianDict}} + supply saved Jacobians + + Returns + ------- + J : dict of {str: dict of {str: array(T,T)}} + J[o][i] for output o and input i gives T*T Jacobian of o with respect to i + """ + # The default set of outputs are all outputs of the backward iteration function + # except for the backward iteration variables themselves + if exogenous is None: + exogenous = list(self.inputs) + if outputs is None or output_list is None: + outputs = self.non_back_iter_outputs + else: + outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) + + relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] + + # if we supply Jacobians, use them if possible, warn if they cannot be used + if Js is not None: + outputs_cap = [o.capitalize() for o in outputs] + if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): + return Js[self.name] + + # step 0: preliminary processing of steady state + (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) + + # step 1 of fake news algorithm + # compute curlyY and curlyD (backward iteration) for each input i + curlyYs, curlyDs = {}, {} + for i in relevant_shocks: + curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, + ss.internal[self.name]['D'], Pi.T.copy(), + sspol_i, sspol_pi, sspol_space, T, h, + ss_for_hetinput) + + # step 2 of fake news algorithm + # compute prediction vectors curlyP (forward iteration) for each outcome o + curlyPs = {} + for o in outputs: + curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) + + # steps 3-4 of fake news algorithm + # make fake news matrix and Jacobian for each outcome-input pair + F, J = {}, {} + for o in outputs: + for i in relevant_shocks: + if o.capitalize() not in F: + F[o.capitalize()] = {} + if o.capitalize() not in J: + J[o.capitalize()] = {} + F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) + J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) + + return JacobianDict(J, name=self.name) + + def add_hetinput(self, hetinput, overwrite=False, verbose=True): + """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process + inputs through the hetinput function. + + A `hetinput` is any non-scalar-valued input argument provided to the HetBlock's backward iteration function, + self.back_step_fun, which is of the same dimensions as the distribution of agents in the HetBlock over + the relevant idiosyncratic state variables, generally referred to as `D`. e.g. The one asset HANK model + example provided in the models directory of sequence_jacobian has a hetinput `T`, which is skill-specific + transfers. + """ + if self.hetinput is not None and overwrite is False: + raise ValueError('Trying to attach hetinput when one already exists!') + else: + if verbose: + if self.hetinput is not None and overwrite is True: + print(f"Overwriting current hetinput, {self.hetinput.__name__} with new hetinput," + f" {hetinput.__name__}!") + else: + print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") + + self.hetinput = hetinput + self.hetinput_inputs = set(utils.misc.input_list(hetinput)) + self.hetinput_outputs = set(utils.misc.output_list(hetinput)) + self.hetinput_outputs_order = utils.misc.output_list(hetinput) + + # modify inputs to include hetinput's additional inputs, remove outputs + self.inputs |= self.hetinput_inputs + self.inputs -= self.hetinput_outputs + + self.internal |= self.hetinput_outputs + + def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): + """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process + inputs through the hetoutput function. + + A `hetoutput` is any *non-scalar-value* output that the user might desire to be calculated from + the output arguments of the HetBlock's backward iteration function. Importantly, as of now the `hetoutput` + cannot be a function of time displaced values of the HetBlock's outputs but rather must be able to + be calculated from the outputs statically. e.g. The two asset HANK model example provided in the models + directory of sequence_jacobian has a hetoutput, `chi`, the adjustment costs for any initial level of assets + `a`, to any new level of assets `a'`. + """ + if self.hetoutput is not None and overwrite is False: + raise ValueError('Trying to attach hetoutput when one already exists!') + else: + if verbose: + if self.hetoutput is not None and overwrite is True: + print(f"Overwriting current hetoutput, {self.hetoutput.name} with new hetoutput," + f" {hetoutput.name}!") + else: + print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") + + self.hetoutput = hetoutput + self.hetoutput_inputs = set(hetoutput.input_list) + self.hetoutput_outputs = set(hetoutput.output_list) + self.hetoutput_outputs_order = hetoutput.output_list + + # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput + # and aggregating the hetoutput, but do not include: + # 1) objects computed within the HetBlock's backward iteration that enter into the hetoutput computation + # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) + # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation + # but is computed after the backward iteration + self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) + # Modify the HetBlock's outputs to include the aggregated hetoutputs + self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) + + self.internal |= self.hetoutput_outputs + + '''Part 3: components of ss(): + - policy_ss : backward iteration to get steady-state policies and other outcomes + - dist_ss : forward iteration to get steady-state distribution and compute aggregates + ''' + + def policy_ss(self, ssin, tol=1E-8, maxit=5000): + """Find steady-state policies and backward variables through backward iteration until convergence. + + Parameters + ---------- + ssin : dict + all steady-state inputs to back_step_fun, including seed values for backward variables + tol : [optional] float + max diff between consecutive iterations of policy variables needed for convergence + maxit : [optional] int + maximum number of iterations, if 'tol' not reached by then, raise error + + Returns + ---------- + sspol : dict + all steady-state outputs of backward iteration, combined with inputs to backward iteration + """ + + # find initial values for backward iteration and account for hetinputs + original_ssin = ssin + ssin = self.make_inputs(ssin) + + old = {} + for it in range(maxit): + try: + # run and store results of backward iteration, which come as tuple, in dict + sspol = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**ssin))} + except KeyError as e: + print(f'Missing input {e} to {self.back_step_fun.__name__}!') + raise + + # only check convergence every 10 iterations for efficiency + if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) + for k in self.policy): + break + + # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration + old.update({k: sspol[k] for k in self.policy}) + ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) + else: + raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') + + # want to record inputs in ssin, but remove _p, add in hetinput inputs if there + for k in self.inputs_to_be_primed: + ssin[k] = ssin[k + '_p'] + del ssin[k + '_p'] + if self.hetinput is not None: + for k in self.hetinput_inputs: + if k in original_ssin: + ssin[k] = original_ssin[k] + return {**ssin, **sspol} + + def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): + """Find steady-state distribution through forward iteration until convergence. + + Parameters + ---------- + Pi : array + steady-state Markov matrix for exogenous variable + sspol : dict + steady-state policies on grid for all policy variables in self.policy + grid : dict + grids for all policy variables in self.policy + tol : [optional] float + absolute tolerance for max diff between consecutive iterations for distribution + maxit : [optional] int + maximum number of iterations, if 'tol' not reached by then, raise error + D_seed : [optional] array + initial seed for overall distribution + pi_seed : [optional] array + initial seed for stationary dist of Pi, if no D_seed + + Returns + ---------- + D : array + steady-state distribution + """ + + # first obtain initial distribution D + if D_seed is None: + # compute stationary distribution for exogenous variable + pi = utils.discretize.stationary(Pi, pi_seed) + + # now initialize full distribution with this, assuming uniform distribution on endogenous vars + endogenous_dims = [grid[k].shape[0] for k in self.policy] + D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) + else: + D = D_seed + + # obtain interpolated policy rule for each dimension of endogenous policy + sspol_i = {} + sspol_pi = {} + for pol in self.policy: + # use robust binary search-based method that only requires grids, not policies, to be monotonic + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], sspol[pol]) + + # iterate until convergence by tol, or maxit + Pi_T = Pi.T.copy() + for it in range(maxit): + Dnew = self.forward_step(D, Pi_T, sspol_i, sspol_pi) + + # only check convergence every 10 iterations for efficiency + if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): + break + D = Dnew + else: + raise ValueError(f'No convergence after {maxit} forward iterations!') + + return D + + '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper + - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs + - Step 2: forward_iteration_fakenews to get curlyPs + - Step 3: build_F to get fake news matrix from curlyYs, curlyDs, curlyPs + - Step 4: J_from_F to get Jacobian from fake news matrix + ''' + + def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): + # shock perturbs outputs + shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, + utils.differentiate.numerical_diff(self.back_step_fun, + ssin_dict, din_dict, h, + ssout_list))} + curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} + + # which affects the distribution tomorrow + pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} + + # Include an additional term to account for the effect of a deleveraging shock affecting the grid + if "delev_exante" in din_dict: + dx = np.zeros_like(sspol_pi["a"]) + dx[sspol_i["a"] == 0] = 1. + add_term = sspol_pi["a"] * dx / sspol_space["a"] + pol_pi_shock["a"] += add_term + + curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) + + # and the aggregate outcomes today + curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} + + return curlyV, curlyD, curlyY + + def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, + sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): + """Iterate policy steps backward T times for a single shock.""" + # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None + # since unless self.hetinput_inputs is exactly equal to input_shocked, calling + # self.hetinput() inside the symmetric differentiation function will throw an error. + # It's probably better/more informative to throw that error out here. + if self.hetinput is not None and input_shocked in self.hetinput_inputs: + # if input_shocked is an input to hetinput, take numerical diff to get response + din_dict = dict(zip(self.hetinput_outputs_order, + utils.differentiate.numerical_diff_symmetric(self.hetinput, + ss_for_hetinput, {input_shocked: 1}, h))) + else: + # otherwise, we just have that one shock + din_dict = {input_shocked: 1} + + # contemporaneous response to unit scalar shock + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) + + # infer dimensions from this and initialize empty arrays + curlyDs = np.empty((T,) + curlyD.shape) + curlyYs = {k: np.empty(T) for k in curlyY.keys()} + + # fill in current effect of shock + curlyDs[0, ...] = curlyD + for k in curlyY.keys(): + curlyYs[k][0] = curlyY[k] + + # fill in anticipation effects + for t in range(1, T): + curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, + output_list, ssin_dict, ssout_list, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) + for k in curlyY.keys(): + curlyYs[k][t] = curlyY[k] + + return curlyYs, curlyDs + + def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): + """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. + + Note we depart from definition in paper by applying the demeaning operator in addition to Lambda + at each step. This does not affect products with curlyD (which are the only way curlyPs enter + Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits + since curlyPs now go to zero for high t (used in paper in proof of Proposition 1). + """ + curlyPs = np.empty((T,) + o_ss.shape) + curlyPs[0, ...] = utils.misc.demean(o_ss) + for t in range(1, T): + curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], + Pi, pol_i_ss, pol_pi_ss)) + return curlyPs + + @staticmethod + def build_F(curlyYs, curlyDs, curlyPs): + T = curlyDs.shape[0] + Tpost = curlyPs.shape[0] - T + 2 + F = np.empty((Tpost + T - 1, T)) + F[0, :] = curlyYs + F[1:, :] = curlyPs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T + return F + + @staticmethod + def J_from_F(F): + J = F.copy() + for t in range(1, J.shape[1]): + J[1:, t] += J[:-1, t - 1] + return J + + '''Part 5: helpers for .jac and .ajac: preliminary processing''' + + def jac_prelim(self, ss): + """Helper that does preliminary processing of steady state for fake news algorithm. + + Parameters + ---------- + ss : dict, all steady-state info, intended to be from .ss() + + Returns + ---------- + ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step + Pi : array (S*S), Markov matrix for exogenous state + ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same + as steady-state numerically since SS convergence was to some tolerance threshold) + ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) + sspol_i : dict, indices on lower bracketing gridpoint for all in self.policy + sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy + sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy + """ + # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation + ssin_dict = self.make_inputs(ss) + Pi = ss[self.exogenous] + grid = {k: ss[k+'_grid'] for k in self.policy} + ssout_list = self.back_step_fun(**ssin_dict) + + ss_for_hetinput = None + if self.hetinput is not None: + ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} + + # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints + sspol_i = {} + sspol_pi = {} + sspol_space = {} + for pol in self.policy: + # use robust binary-search-based method that only requires grids to be monotonic + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) + sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] + + toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) + + return toreturn + + '''Part 6: helper to extract inputs and potentially process them through hetinput''' + + def make_inputs(self, back_step_inputs_dict): + """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, + process stuff through self.hetinput first if it's there. + """ + input_dict = copy.deepcopy(back_step_inputs_dict) + + # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady + # state computation as well as for Jacobian/impulse evaluation for HetBlocks, + # where in the former the hetinputs and value function have yet to be computed, + # whereas in the latter they have already been computed + # and hence do not need to be recomputed. There may be room to clean this function up a bit. + if isinstance(back_step_inputs_dict, SteadyStateDict): + input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) + input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) + else: + # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include + # them as inputs for self.back_step_fun + if self.hetinput is not None: + outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] + for k in self.hetinput_inputs if k in input_dict})) + input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) + + # Check if there are entries in indict corresponding to self.inputs_to_be_primed. + # In particular, we are interested in knowing if an initial value + # for the backward iteration variable has been provided. + # If it has not been provided, then use self.backward_init to calculate the initial values. + if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): + initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] + input_dict.update(zip(utils.misc.output_list(self.backward_init), + utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) + + for i_p in self.inputs_to_be_primed: + input_dict[i_p + "_p"] = input_dict[i_p] + del input_dict[i_p] + + try: + return {k: input_dict[k] for k in self.back_step_inputs if k in input_dict} + except KeyError as e: + print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') + raise + + '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' + + def forward_step(self, D, Pi_T, pol_i, pol_pi): + """Update distribution, calling on 1d and 2d-specific compiled routines. + + Parameters + ---------- + D : array, beginning-of-period distribution + Pi_T : array, transpose Markov matrix + pol_i : dict, indices on lower bracketing gridpoint for all in self.policy + pol_pi : dict, weights on lower bracketing gridpoint for all in self.policy + + Returns + ---------- + Dnew : array, beginning-of-next-period distribution + """ + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_1d(D, Pi_T, pol_i[p], pol_pi[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_2d(D, Pi_T, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + def forward_step_transpose(self, D, Pi, pol_i, pol_pi): + """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_transpose_1d(D, Pi, pol_i[p], pol_pi[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_transpose_2d(D, Pi, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): + """Forward_step linearized with respect to pol_pi""" + if len(self.policy) == 1: + p, = self.policy + return utils.forward_step.forward_step_shock_1d(Dss, Pi_T, pol_i_ss[p], pol_pi_shock[p]) + elif len(self.policy) == 2: + p1, p2 = self.policy + return utils.forward_step.forward_step_shock_2d(Dss, Pi_T, pol_i_ss[p1], pol_i_ss[p2], + pol_pi_ss[p1], pol_pi_ss[p2], + pol_pi_shock[p1], pol_pi_shock[p2]) + else: + raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + + +def hetoutput(custom_aggregation=None): + def decorator(f): + return HetOutput(f, custom_aggregation=custom_aggregation) + return decorator + + +class HetOutput: + def __init__(self, f, custom_aggregation=None): + self.name = f.__name__ + self.f = f + self.eval_input_list = utils.misc.input_list(f) + + self.custom_aggregation = custom_aggregation + self.agg_input_list = [] if custom_aggregation is None else utils.misc.input_list(custom_aggregation) + + # We are distinguishing between the eval_input_list and agg_input_list because custom aggregation may require + # certain arguments that are not required for simply evaluating the hetoutput + self.input_list = list(set(self.eval_input_list).union(set(self.agg_input_list))) + self.output_list = utils.misc.output_list(f) + + def evaluate(self, arg_dict): + hetoutputs = dict(zip(self.output_list, utils.misc.make_tuple(self.f(*[arg_dict[i] for i + in self.eval_input_list])))) + return hetoutputs + + def aggregate(self, hetoutputs, D, custom_aggregation_args, mode="ss"): + if self.custom_aggregation is not None: + hetoutputs_w_std_aggregation = list(set(self.output_list) - + set([utils.misc.uncapitalize(o) for o + in utils.misc.output_list(self.custom_aggregation)])) + hetoutputs_w_custom_aggregation = list(set(self.output_list) - set(hetoutputs_w_std_aggregation)) + else: + hetoutputs_w_std_aggregation = self.output_list + hetoutputs_w_custom_aggregation = [] + + # TODO: May need to check if this works properly for td + if self.custom_aggregation is not None: + hetoutputs_w_custom_aggregation_args = dict(zip(hetoutputs_w_custom_aggregation, + [hetoutputs[i] for i in hetoutputs_w_custom_aggregation])) + custom_agg_inputs = {"D": D, **hetoutputs_w_custom_aggregation_args, **custom_aggregation_args} + custom_aggregates = dict(zip([o.capitalize() for o in hetoutputs_w_custom_aggregation], + utils.misc.make_tuple(self.custom_aggregation(*[custom_agg_inputs[i] for i + in self.agg_input_list])))) + else: + custom_aggregates = {} + + if mode == "ss": + std_aggregates = {o.capitalize(): np.vdot(D, hetoutputs[o]) for o in hetoutputs_w_std_aggregation} + elif mode == "td": + std_aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D, hetoutputs[o]) + for o in hetoutputs_w_std_aggregation} + else: + raise RuntimeError(f"Mode {mode} is not supported in HetOutput aggregation. Choose either 'ss' or 'td'") + + return {**std_aggregates, **custom_aggregates} From 5c8a20dfd712525d7b6230277935a04c13f33e42 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 8 Jun 2021 13:44:08 -0500 Subject: [PATCH 158/288] DiscontBlock: steady state works. --- .../{hetdc_block.py => discont_block.py} | 225 +++++++++--------- .../utilities/forward_step.py | 41 ++++ src/sequence_jacobian/utilities/misc.py | 37 +++ 3 files changed, 186 insertions(+), 117 deletions(-) rename src/sequence_jacobian/blocks/{hetdc_block.py => discont_block.py} (86%) diff --git a/src/sequence_jacobian/blocks/hetdc_block.py b/src/sequence_jacobian/blocks/discont_block.py similarity index 86% rename from src/sequence_jacobian/blocks/hetdc_block.py rename to src/sequence_jacobian/blocks/discont_block.py index 259d4d8..8194365 100644 --- a/src/sequence_jacobian/blocks/hetdc_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -11,14 +11,14 @@ from ..utilities.misc import verify_saved_jacobian -def het(exogenous, policy, backward, backward_init=None): +def discont(exogenous, policy, disc_policy, backward, backward_init=None): def decorator(back_step_fun): - return HetBlock(back_step_fun, exogenous, policy, backward, backward_init=backward_init) + return DiscontBlock(back_step_fun, exogenous, policy, disc_policy, backward, backward_init=backward_init) return decorator -class HetBlock(Block): - """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. +class DiscontBlock(Block): + """Part 1: Initializer for DiscontBlock, intended to be called via @hetdc() decorator on backward step function. IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle @@ -26,7 +26,7 @@ class HetBlock(Block): of the `policy` and non-aggregate output variables specified in the backward step function. """ - def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None): + def __init__(self, back_step_fun, exogenous, policy, disc_policy, backward, backward_init=None): """Construct HetBlock from backward iteration function. Parameters @@ -39,6 +39,8 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non policy : str or sequence of str names of policy variables of endogenous, continuous state variables e.g. assets 'a', must be returned by function + disc_policy: str + name of policy function for discrete choices (probabilities) backward : str or sequence of str variables that together comprise the 'v' that we use for iterating backward must appear both as outputs and as arguments @@ -68,27 +70,29 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) # See the docstring of HetBlock for details on the attributes directly below - self.exogenous = exogenous - self.policy, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (policy, backward)) + self.disc_policy = disc_policy + self.policy = policy + self.exogenous, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (exogenous, backward)) # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to # the primed key name, such that self.back_step_fun will properly call those variables. # e.g. the key "Va" will become "Va_p", associated to the same value. - self.inputs_to_be_primed = {self.exogenous} | set(self.back_iter_vars) + self.inputs_to_be_primed = set(self.exogenous) | set(self.back_iter_vars) # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward # iteration variables themselves. - self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) + self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) - set(self.disc_policy) # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used # in utils.graph.block_sort to topologically sort blocks along the DAG # according to their aggregate outputs and inputs. self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} - self.inputs.remove(exogenous + '_p') - self.inputs.add(exogenous) + for ex in self.exogenous: + self.inputs.remove(ex + '_p') + self.inputs.add(ex) # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. @@ -107,18 +111,18 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # as opposed to being available at the top level self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} - if len(self.policy) > 2: - raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") + if len(self.policy) > 1: + raise ValueError(f"More than one continuous states in {back_step_fun.__name__}, not yet supported") # Checking that the various inputs/outputs attributes are correctly set - if self.exogenous + '_p' not in self.back_step_inputs: - raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") + for ex in self.exogenous: + if ex + '_p' not in self.back_step_inputs: + raise ValueError(f"Markov matrix '{ex}_p' not included as argument in {back_step_fun.__name__}") - for pol in self.policy: - if pol not in self.back_step_outputs: - raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") - if pol[0].isupper(): - raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") + if self.policy not in self.back_step_outputs: + raise ValueError(f"Policy '{self.policy}' not included as output in {back_step_fun.__name__}") + if self.policy[0].isupper(): + raise ValueError(f"Policy '{self.policy}' is uppercase in {back_step_fun.__name__}, which is not allowed") for back in self.back_iter_vars: if back + '_p' not in self.back_step_inputs: @@ -145,12 +149,12 @@ def __repr__(self): """Nice string representation of HetBlock for printing to console""" if self.hetinput is not None: if self.hetoutput is not None: - return f"" else: - return f"" + return f"" else: - return f"" + return f"" '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - ss : do backward and forward iteration until convergence to get complete steady state @@ -163,23 +167,6 @@ def __repr__(self): each backward iteration step in td ''' - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, **kwargs) - - def jac(self, ss, shock_list=None, T=None, **kwargs): - if shock_list is None: - shock_list = list(self.inputs) - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, shock_list, T, **kwargs) def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): @@ -222,17 +209,15 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, ss = copy.deepcopy(calibration) # extract information from calibration - Pi = calibration[self.exogenous] - grid = {k: calibration[k+'_grid'] for k in self.policy} + grid = calibration[self.policy + '_grid'] D_seed = calibration.get('D', None) - pi_seed = calibration.get(self.exogenous + '_seed', None) # run backward iteration sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) ss.update(sspol) # run forward iteration - D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) + D = self.dist_ss(sspol, grid, forward_tol, forward_maxit, D_seed) ss.update({"D": D}) # aggregate all outputs other than backward variables on grid, capitalize @@ -250,11 +235,9 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict(ss, internal=self) def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): - """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, + """Evaluate transitional dynamics for DiscontBlock given dynamic paths for inputs in exogenous, assuming that we start and end in steady state ss, and that all inputs not specified in - kwargs are constant at their ss values. Analog to SimpleBlock.td. - - CANNOT provide time-varying paths of grid or Markov transition matrix for now. + exogenous are constant at their ss values. Analog to SimpleBlock.td. Parameters ---------- @@ -286,24 +269,33 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal T = shock_lengths[0] # copy from ss info - Pi_T = ss[self.exogenous].T.copy() - D = ss.internal[self.name]['D'] + D, P = ss['D'], ss[self.disc_policy] # construct grids for policy variables either from the steady state grid if the grid is meant to be # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. - grid = {} - use_ss_grid = {} - for k in self.policy: - if grid_paths is not None and k in grid_paths: - grid[k] = grid_paths[k] - use_ss_grid[k] = False - else: - grid[k] = ss[k+"_grid"] - use_ss_grid[k] = True + if grid_paths is not None and self.policy in grid_paths: + grid = grid_paths[self.policy] + use_ss_grid = False + else: + grid = ss[self.policy + "_grid"] + use_ss_grid = True + # sspol_i, sspol_pi = utils.interpolate_coord_robust(grid, ss[self.policy]) # allocate empty arrays to store result, assume all like D individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} + P_path = np.empty((T,) + P.shape) + + # obtain full path of multidimensional inputs + multidim_inputs = {k: np.empty((T,) + ss[k].shape) for k in self.hetinput_outputs_order} + if self.hetinput is not None: + indict = dict(ss.items()) + for t in range(T): + indict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + hetout = dict(zip(self.hetinput_outputs_order, + self.hetinput(**{k: indict[k] for k in self.hetinput_inputs}))) + for k in self.hetinput_outputs_order: + multidim_inputs[k][t, ...] = hetout[k] # backward iteration backdict = dict(ss.items()) @@ -311,39 +303,56 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + + # add in multidimensional inputs EXCEPT exogenous state transitions (at lead 0) + backdict.update({k: ss[k] + v[t, ...] for k, v in multidim_inputs.items() if k not in self.exogenous}) + + # add in multidimensional inputs FOR exogenous state transitions (at lead 1) + if t < T - 1: + backdict.update({k: ss[k] + v[t+1, ...] for k, v in multidim_inputs.items() if k in self.exogenous}) + + # step back individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} + + # update backward variables backdict.update({k: individual[k] for k in self.back_iter_vars}) + # compute hetoutputs if self.hetoutput is not None: hetoutput = self.hetoutput.evaluate(backdict) for k in self.hetoutput_outputs: hetoutput_paths[k][t, ...] = hetoutput[k] + # save individual outputs of interest + P_path[t, ...] = individual[self.disc_policy] for k in self.non_back_iter_outputs: individual_paths[k][t, ...] = individual[k] + # forward iteration + # initial markov matrix (may have been shocked) + Pi_path = [[multidim_inputs[k][0, ...] if k in self.hetinput_outputs_order else ss[k] for k in self.exogenous]] + + # on impact: assets are predetermined, but Pi could be shocked, and P can change D_path = np.empty((T,) + D.shape) - D_path[0, ...] = D + if use_ss_grid: + grid_var = grid + else: + grid_var = grid[0, ...] + sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid_var, ss[self.policy]) + D_path[0, ...] = self.forward_step(D, P_path[0, ...], Pi_path[0], sspol_i, sspol_pi) for t in range(T-1): # have to interpolate policy separately for each t to get sparse transition matrices - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - if use_ss_grid[pol]: - grid_var = grid[pol] - else: - grid_var = grid[pol][t, ...] - if monotonic: - # TODO: change for two-asset case so assumption is monotonicity in own asset, not anything else - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord(grid_var, - individual_paths[pol][t, ...]) - else: - sspol_i[pol], sspol_pi[pol] =\ - utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[pol][t, ...]) + if not use_ss_grid: + grid_var = grid[t, ...] + pol_i, pol_pi = utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[self.policy][t, ...]) + + # update exogenous Markov matrices + Pi = [multidim_inputs[k][t+1, ...] if k in self.hetinput_outputs_order else ss[k] for k in self.exogenous] + Pi_path.append(Pi) # step forward - D_path[t+1, ...] = self.forward_step(D_path[t, ...], Pi_T, sspol_i, sspol_pi) + D_path[t+1, ...] = self.forward_step(D_path[t, ...], P_path[t+1, ...], Pi_path[t+1, ...], pol_i, pol_pi) # obtain aggregates of all outputs, made uppercase aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) @@ -355,8 +364,8 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal # return either this, or also include distributional information if returnindividual: - return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, - 'D': D_path}) - ss + return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **multidim_inputs, + **hetoutput_paths, 'D': D_path, 'P_path': P_path, 'Pi_path': Pi_path}) - ss else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss @@ -369,6 +378,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) + # TODO: update this def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. @@ -549,11 +559,11 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): # only check convergence every 10 iterations for efficiency if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) - for k in self.policy): + for k in self.back_iter_vars): break # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration - old.update({k: sspol[k] for k in self.policy}) + old.update({k: sspol[k] for k in self.back_iter_vars}) ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) else: raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') @@ -568,13 +578,11 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): ssin[k] = original_ssin[k] return {**ssin, **sspol} - def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): + def dist_ss(self, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None): """Find steady-state distribution through forward iteration until convergence. Parameters ---------- - Pi : array - steady-state Markov matrix for exogenous variable sspol : dict steady-state policies on grid for all policy variables in self.policy grid : dict @@ -585,37 +593,29 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see maximum number of iterations, if 'tol' not reached by then, raise error D_seed : [optional] array initial seed for overall distribution - pi_seed : [optional] array - initial seed for stationary dist of Pi, if no D_seed Returns ---------- D : array steady-state distribution """ + # extract transition matrix for exogenous states + Pi = [sspol[k] for k in self.exogenous] + P = sspol[self.disc_policy] # first obtain initial distribution D if D_seed is None: - # compute stationary distribution for exogenous variable - pi = utils.discretize.stationary(Pi, pi_seed) - - # now initialize full distribution with this, assuming uniform distribution on endogenous vars - endogenous_dims = [grid[k].shape[0] for k in self.policy] - D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) + # initialize at uniform distribution + D = np.ones_like(sspol[self.policy]) / sspol[self.policy].size else: D = D_seed # obtain interpolated policy rule for each dimension of endogenous policy - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - # use robust binary search-based method that only requires grids, not policies, to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], sspol[pol]) + sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid, sspol[self.policy]) # iterate until convergence by tol, or maxit - Pi_T = Pi.T.copy() for it in range(maxit): - Dnew = self.forward_step(D, Pi_T, sspol_i, sspol_pi) + Dnew = self.forward_step(D, P, Pi, sspol_i, sspol_pi) # only check convergence every 10 iterations for efficiency if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): @@ -817,28 +817,19 @@ def make_inputs(self, back_step_inputs_dict): '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' - def forward_step(self, D, Pi_T, pol_i, pol_pi): - """Update distribution, calling on 1d and 2d-specific compiled routines. - Parameters - ---------- - D : array, beginning-of-period distribution - Pi_T : array, transpose Markov matrix - pol_i : dict, indices on lower bracketing gridpoint for all in self.policy - pol_pi : dict, weights on lower bracketing gridpoint for all in self.policy + def forward_step(self, D3_prev, P, Pi, a_i, a_pi): + """Update distribution from (s[0], z[0], a[-1]) to (s[1], z[1], a[0])""" + # update with continuous policy of last period + D4 = utils.forward_step.forward_step_cpol(D3_prev, a_i, a_pi) + + # update with exogenous shocks today + D2 = utils.forward_step.forward_step_exo(D4, Pi) + + # update with discrete choice today + D3 = utils.forward_step.forward_step_dpol(D2, P) + return D3 - Returns - ---------- - Dnew : array, beginning-of-next-period distribution - """ - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_1d(D, Pi_T, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_2d(D, Pi_T, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") def forward_step_transpose(self, D, Pi, pol_i, pol_pi): """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" diff --git a/src/sequence_jacobian/utilities/forward_step.py b/src/sequence_jacobian/utilities/forward_step.py index a80b7bf..3d73a32 100644 --- a/src/sequence_jacobian/utilities/forward_step.py +++ b/src/sequence_jacobian/utilities/forward_step.py @@ -171,3 +171,44 @@ def forward_step_transpose_endo_2d(D, x_i, y_i, x_pi, y_pi): (1-alpha) * beta * D[iz, ixp+1, iyp] + (1-alpha) * (1-beta) * D[iz, ixp+1, iyp+1]) return Dnew + + +''' +For HetDC block. + +D0 : s[-1], z[-1], a[-1] (end of last period) +D1 : x[0], z[-1], a[-1] (employment shock) +D2 : x[0], z[0], a[-1] (productivity shock) +D3 : s[0], z[0], a[-1] (discrete choice) REFERENCE STAGE +D4 : s[0], z[0], a[0] (cont choice) + +''' + + +def forward_step_exo(D0, Pi): + """Update distribution s[-1], z[-1], a[-1] to x[0], z[0], a[-1].""" + D1 = np.einsum('sza,sx->xza', D0, Pi[0]) # s[-1] -> x[0] + D2 = np.einsum('xza,zp->xpa', D1, Pi[1]) # z[-1] -> z[0] + return D2 + + +def forward_step_dpol(D2, P): + """Update distribution x[0], z[0], a[-1] to s[0], z[0], a[-1].""" + D3 = np.einsum('xza,xsza->sza', D2, P) + return D3 + + +@njit +def forward_step_cpol(D3, a_i, a_pi): + """Update distribution s[0], z[0], a[-1] to s[0], z[0], a[0].""" + nS, nZ, nA = D3.shape + D4 = np.zeros_like(D3) + for iw in range(nS): + for iz in range(nZ): + for ia in range(nA): + i = a_i[iw, iz, ia] + pi = a_pi[iw, iz, ia] + d = D3[iw, iz, ia] + D4[iw, iz, i] += d * pi + D4[iw, iz, i+1] += d * (1 - pi) + return D4 \ No newline at end of file diff --git a/src/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py index cf47e7b..8ff9ff0 100644 --- a/src/sequence_jacobian/utilities/misc.py +++ b/src/sequence_jacobian/utilities/misc.py @@ -173,3 +173,40 @@ def verify_saved_jacobian(block_name, Js, outputs, inputs, T): return False return True + + +'''Tools for taste shocks used in discrete choice problems''' + + +def choice_prob(vfun, lam): + """ + Logit choice probability of choosing along first axis. + + Parameters + ---------- + vfun : array(Ns, Nz, Na): discrete choice specific value function + lam : float, scale of taste shock + + Returns + ------- + prob : array (Ns, Nz, nA): choice probability + """ + # rescale values for numeric robustness + vmax = np.max(vfun, axis=0) + vfun_norm = vfun - vmax + + # apply formula (could be njitted in separate function) + P = np.exp(vfun_norm / lam) / np.sum(np.exp(vfun_norm / lam), axis=0) + return P + + +def logsum(vfun, lam): + """Logsum formula for expected continuation value.""" + + # rescale values for numeric robustness + vmax = np.max(vfun, axis=0) + vfun_norm = vfun - vmax + + # apply formula (could be njitted in separate function) + VE = vmax + lam * np.log(np.sum(np.exp(vfun_norm / lam), axis=0)) + return VE From 8d5baa359ff4307cdc1851b757b9c33b3e919a59 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 8 Jun 2021 14:08:54 -0500 Subject: [PATCH 159/288] DiscontBlock: impulse_nonlinear works. --- src/sequence_jacobian/blocks/discont_block.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index 8194365..f43b1e6 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -234,7 +234,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict(ss, internal=self) - def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): + def impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for DiscontBlock given dynamic paths for inputs in exogenous, assuming that we start and end in steady state ss, and that all inputs not specified in exogenous are constant at their ss values. Analog to SimpleBlock.td. @@ -246,9 +246,6 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal exogenous : dict of {str : array(T, ...)} all time-varying inputs here (in deviations), with first dimension being time this must have same length T for all entries (all outputs will be calculated up to T) - monotonic : [optional] bool - flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us - to use faster interpolation routines, otherwise use slower robust to nonmonotonicity returnindividual : [optional] bool return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} @@ -269,7 +266,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal T = shock_lengths[0] # copy from ss info - D, P = ss['D'], ss[self.disc_policy] + D, P = ss.internal[self.name]['D'], ss.internal[self.name][self.disc_policy] # construct grids for policy variables either from the steady state grid if the grid is meant to be # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. @@ -287,7 +284,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal P_path = np.empty((T,) + P.shape) # obtain full path of multidimensional inputs - multidim_inputs = {k: np.empty((T,) + ss[k].shape) for k in self.hetinput_outputs_order} + multidim_inputs = {k: np.empty((T,) + ss.internal[self.name][k].shape) for k in self.hetinput_outputs_order} if self.hetinput is not None: indict = dict(ss.items()) for t in range(T): @@ -305,11 +302,11 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) # add in multidimensional inputs EXCEPT exogenous state transitions (at lead 0) - backdict.update({k: ss[k] + v[t, ...] for k, v in multidim_inputs.items() if k not in self.exogenous}) + backdict.update({k: ss.internal[self.name][k] + v[t, ...] for k, v in multidim_inputs.items() if k not in self.exogenous}) # add in multidimensional inputs FOR exogenous state transitions (at lead 1) if t < T - 1: - backdict.update({k: ss[k] + v[t+1, ...] for k, v in multidim_inputs.items() if k in self.exogenous}) + backdict.update({k: ss.internal[self.name][k] + v[t+1, ...] for k, v in multidim_inputs.items() if k in self.exogenous}) # step back individual = {k: v for k, v in zip(self.back_step_output_list, @@ -339,7 +336,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal grid_var = grid else: grid_var = grid[0, ...] - sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid_var, ss[self.policy]) + sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid_var, ss.internal[self.name][self.policy]) D_path[0, ...] = self.forward_step(D, P_path[0, ...], Pi_path[0], sspol_i, sspol_pi) for t in range(T-1): # have to interpolate policy separately for each t to get sparse transition matrices @@ -352,7 +349,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal Pi_path.append(Pi) # step forward - D_path[t+1, ...] = self.forward_step(D_path[t, ...], P_path[t+1, ...], Pi_path[t+1, ...], pol_i, pol_pi) + D_path[t+1, ...] = self.forward_step(D_path[t, ...], P_path[t+1, ...], Pi, pol_i, pol_pi) # obtain aggregates of all outputs, made uppercase aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) From 6444a0739fc37229ec6b087af9a96b7a2a3237aa Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 8 Jun 2021 18:21:48 -0500 Subject: [PATCH 160/288] DiscontBlock: jacobian works. Ready to roll! --- src/sequence_jacobian/blocks/discont_block.py | 238 ++++++++++-------- .../utilities/forward_step.py | 31 ++- 2 files changed, 167 insertions(+), 102 deletions(-) diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index f43b1e6..f0e3873 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -7,7 +7,6 @@ from .. import utilities as utils from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict -from ..devtools.deprecate import rename_output_list_to_outputs from ..utilities.misc import verify_saved_jacobian @@ -375,8 +374,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - # TODO: update this - def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): + def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -404,10 +402,8 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js # except for the backward iteration variables themselves if exogenous is None: exogenous = list(self.inputs) - if outputs is None or output_list is None: + if outputs is None: outputs = self.non_back_iter_outputs - else: - outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] @@ -418,22 +414,23 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js return Js[self.name] # step 0: preliminary processing of steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) + (ssin_dict, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space, D0, D2, Pi, P) = self.jac_prelim(ss) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i - curlyYs, curlyDs = {}, {} + dYs, dDs, dD_ps, dD_direct = {}, {}, {}, {} for i in relevant_shocks: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, - ss.internal[self.name]['D'], Pi.T.copy(), - sspol_i, sspol_pi, sspol_space, T, h, - ss_for_hetinput) + dYs[i], dDs[i], dD_ps[i], dD_direct[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, + ss.internal[self.name]['D'], + D0, D2, P, Pi, sspol_i, sspol_pi, + sspol_space, T, h, + ss_for_hetinput) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o curlyPs = {} for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) + curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, P, sspol_i, sspol_pi, T) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair @@ -444,8 +441,8 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js F[o.capitalize()] = {} if o.capitalize() not in J: J[o.capitalize()] = {} - F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) - J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) + F[o.capitalize()][i] = DiscontBlock.build_F(dYs[i][o], dD_ps[i], curlyPs[o], dD_direct[i], dDs[i]) + J[o.capitalize()][i] = DiscontBlock.J_from_F(F[o.capitalize()][i]) return JacobianDict(J, name=self.name) @@ -630,72 +627,109 @@ def dist_ss(self, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None): - Step 4: J_from_F to get Jacobian from fake news matrix ''' + def shock_timing(self, input_shocked, D0, Pi_ss, P, ss_for_hetinput, h): + """Figure out the details of how the scalar shock feeds into back_step_fun. + + Main complication: shocks to Pi transmit via hetinput with a lead of 1. + """ + if self.hetinput is not None and input_shocked in self.hetinput_inputs: + # if input_shocked is an input to hetinput, take numerical diff to get response + din_dict = dict(zip(self.hetinput_outputs_order, + utils.differentiate.numerical_diff_symmetric(self.hetinput, ss_for_hetinput, + {input_shocked: 1}, h))) + + if all(k not in din_dict.keys() for k in self.exogenous): + # if Pi is not generated by hetinput, no work to be done + lead = 0 + dD3_direct = None + elif all(np.count_nonzero(din_dict[k]) == 0 for k in self.exogenous if k in din_dict): + # if Pi is generated by hetinput but input_shocked does not affect it, replace Pi with Pi_p + lead = 0 + dD3_direct = None + for k in self.exogenous: + if k in din_dict.keys(): + din_dict[k + '_p'] = din_dict.pop(k) + else: + # if Pi is generated by hetinput and input_shocked affects it, replace that with Pi_p at lead 1 + lead = 1 + Pi = [din_dict[k] if k in din_dict else Pi_ss[num] for num, k in enumerate(self.exogenous)] + dD2_direct = utils.forward_step.forward_step_exo(D0, Pi) + dD3_direct = utils.forward_step.forward_step_dpol(dD2_direct, P) + for k in self.exogenous: + if k in din_dict.keys(): + din_dict[k + '_p'] = din_dict.pop(k) + else: + # if input_shocked feeds directly into back_step_fun with lead 0, no work to be done + lead = 0 + din_dict = {input_shocked: 1} + dD3_direct = None + + return din_dict, lead, dD3_direct + def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): - # shock perturbs outputs + Dss, D2, P, Pi, sspol_i, sspol_pi, sspol_space, h=1E-4): + # 1. shock perturbs outputs shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, utils.differentiate.numerical_diff(self.back_step_fun, ssin_dict, din_dict, h, ssout_list))} - curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} - - # which affects the distribution tomorrow - pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} + dV = {k: shocked_outputs[k] for k in self.back_iter_vars} - # Include an additional term to account for the effect of a deleveraging shock affecting the grid + # 2. which affects the distribution tomorrow via the savings policy + pol_pi_shock = -shocked_outputs[self.policy] / sspol_space if "delev_exante" in din_dict: - dx = np.zeros_like(sspol_pi["a"]) - dx[sspol_i["a"] == 0] = 1. - add_term = sspol_pi["a"] * dx / sspol_space["a"] - pol_pi_shock["a"] += add_term + # include an additional term to account for the effect of a deleveraging shock affecting the grid + dx = np.zeros_like(sspol_pi) + dx[sspol_i == 0] = 1. + add_term = sspol_pi * dx / sspol_space + pol_pi_shock += add_term + dD3_p = self.forward_step_shock(Dss, sspol_i, pol_pi_shock, Pi, P) - curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) + # 3. and the distribution today (and Dmid tomorrow) via the discrete choice + P_shock = shocked_outputs[self.disc_policy] + dD3 = utils.forward_step.forward_step_dpol(D2, P_shock) # s[0], z[0], a[-1] - # and the aggregate outcomes today - curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} + # 4. and the aggregate outcomes today (ignoring dD and dD_direct) + dY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} - return curlyV, curlyD, curlyY + return dV, dD3, dD3_p, dY - def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, + def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, D0, D2, P, Pi, sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): """Iterate policy steps backward T times for a single shock.""" - # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None - # since unless self.hetinput_inputs is exactly equal to input_shocked, calling - # self.hetinput() inside the symmetric differentiation function will throw an error. - # It's probably better/more informative to throw that error out here. - if self.hetinput is not None and input_shocked in self.hetinput_inputs: - # if input_shocked is an input to hetinput, take numerical diff to get response - din_dict = dict(zip(self.hetinput_outputs_order, - utils.differentiate.numerical_diff_symmetric(self.hetinput, - ss_for_hetinput, {input_shocked: 1}, h))) - else: - # otherwise, we just have that one shock - din_dict = {input_shocked: 1} + # map name of shocked input into a perturbation of the inputs of back_step_fun + din_dict, lead, dD_direct = self.shock_timing(input_shocked, D0, Pi.copy(), P, ss_for_hetinput, h) # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) + dV, dD, dD_p, dY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, + Dss, D2, P, Pi, sspol_i, sspol_pi, sspol_space, h=h) # infer dimensions from this and initialize empty arrays - curlyDs = np.empty((T,) + curlyD.shape) - curlyYs = {k: np.empty(T) for k in curlyY.keys()} + dDs = np.empty((T,) + dD.shape) + dD_ps = np.empty((T,) + dD_p.shape) + dYs = {k: np.empty(T) for k in dY.keys()} - # fill in current effect of shock - curlyDs[0, ...] = curlyD - for k in curlyY.keys(): - curlyYs[k][0] = curlyY[k] + # fill in current effect of shock (be careful to handle lead = 1) + dDs[:lead, ...], dD_ps[:lead, ...] = 0, 0 + dDs[lead, ...], dD_ps[lead, ...] = dD, dD_p + for k in dY.keys(): + dYs[k][:lead] = 0 + dYs[k][lead] = dY[k] # fill in anticipation effects - for t in range(1, T): - curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) - for k in curlyY.keys(): - curlyYs[k][t] = curlyY[k] + for t in range(lead + 1, T): + dV, dDs[t, ...], dD_ps[t, ...], dY = self.backward_step_fakenews({k + '_p': + v for k, v in dV.items()}, + output_list, ssin_dict, ssout_list, + Dss, D2, P, Pi, sspol_i, sspol_pi, + sspol_space, h) + + for k in dY.keys(): + dYs[k][t] = dY[k] - return curlyYs, curlyDs + return dYs, dDs, dD_ps, dD_direct - def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): + def forward_iteration_fakenews(self, o_ss, Pi, P, pol_i_ss, pol_pi_ss, T): """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. Note we depart from definition in paper by applying the demeaning operator in addition to Lambda @@ -707,16 +741,26 @@ def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): curlyPs[0, ...] = utils.misc.demean(o_ss) for t in range(1, T): curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], - Pi, pol_i_ss, pol_pi_ss)) + P, Pi, pol_i_ss, pol_pi_ss)) return curlyPs @staticmethod - def build_F(curlyYs, curlyDs, curlyPs): - T = curlyDs.shape[0] - Tpost = curlyPs.shape[0] - T + 2 - F = np.empty((Tpost + T - 1, T)) - F[0, :] = curlyYs - F[1:, :] = curlyPs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T + def build_F(dYs, dD_ps, curlyPs, dD_direct, dDs): + T = dYs.shape[0] + F = np.empty((T, T)) + + # standard effect + F[0, :] = dYs + F[1:, :] = curlyPs[:T-1, ...].reshape((T-1, -1)) @ dD_ps.reshape((T, -1)).T + + # contemporaneous effect via discrete choice + if dDs is not None: + F += curlyPs.reshape((T, -1)) @ dDs.reshape((T, -1)).T + + # direct effect of shock + if dD_direct is not None: + F[:, 0] += curlyPs.reshape((T, -1)) @ dD_direct.ravel() + return F @staticmethod @@ -738,7 +782,7 @@ def jac_prelim(self, ss): Returns ---------- ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step - Pi : array (S*S), Markov matrix for exogenous state + D0 : array (nS, nZ, nA), distribution over s[-1], z[-1], a[-1] ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same as steady-state numerically since SS convergence was to some tolerance threshold) ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) @@ -748,24 +792,26 @@ def jac_prelim(self, ss): """ # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation ssin_dict = self.make_inputs(ss) - Pi = ss[self.exogenous] - grid = {k: ss[k+'_grid'] for k in self.policy} ssout_list = self.back_step_fun(**ssin_dict) ss_for_hetinput = None if self.hetinput is not None: ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} - # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints - sspol_i = {} - sspol_pi = {} - sspol_space = {} - for pol in self.policy: - # use robust binary-search-based method that only requires grids to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) - sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] + # preliminary b: get sparse representations of policy rules and distance between neighboring policy gridpoints + grid = ss[self.policy + '_grid'] + sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid, ss.internal[self.name][self.policy]) + sspol_space = grid[sspol_i + 1] - grid[sspol_i] - toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) + # preliminary c: get end-of-period distribution, need it when Pi is shocked + Pi = [ss.internal[self.name][k] for k in self.exogenous] + D = ss.internal[self.name]['D'] + D0 = utils.forward_step.forward_step_cpol(D, sspol_i, sspol_pi) + D2 = utils.forward_step.forward_step_exo(D0, Pi) + + Pss = ss.internal[self.name][self.disc_policy] + + toreturn = (ssin_dict, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space, D0, D2, Pi, Pss) return toreturn @@ -827,30 +873,20 @@ def forward_step(self, D3_prev, P, Pi, a_i, a_pi): D3 = utils.forward_step.forward_step_dpol(D2, P) return D3 + def forward_step_shock(self, D0, pol_i, pol_pi_shock, Pi, P): + """Forward_step linearized wrt pol_pi.""" + D4 = utils.forward_step.forward_step_cpol_shock(D0, pol_i, pol_pi_shock) + D2 = utils.forward_step.forward_step_exo(D4, Pi) + D3 = utils.forward_step.forward_step_dpol(D2, P) + return D3 - def forward_step_transpose(self, D, Pi, pol_i, pol_pi): - """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_transpose_1d(D, Pi, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_transpose_2d(D, Pi, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): - """Forward_step linearized with respect to pol_pi""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_shock_1d(Dss, Pi_T, pol_i_ss[p], pol_pi_shock[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_shock_2d(Dss, Pi_T, pol_i_ss[p1], pol_i_ss[p2], - pol_pi_ss[p1], pol_pi_ss[p2], - pol_pi_shock[p1], pol_pi_shock[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") + def forward_step_transpose(self, D, P, Pi, a_i, a_pi): + """Transpose of forward_step.""" + D1 = np.einsum('sza,xsza->xza', D, P) + D2 = np.einsum('xpa,zp->xza', D1, Pi[1]) + D3 = np.einsum('xza,sx->sza', D2, Pi[0]) + D4 = utils.forward_step.forward_step_cpol_transpose(D3, a_i, a_pi) + return D4 def hetoutput(custom_aggregation=None): diff --git a/src/sequence_jacobian/utilities/forward_step.py b/src/sequence_jacobian/utilities/forward_step.py index 3d73a32..50d96db 100644 --- a/src/sequence_jacobian/utilities/forward_step.py +++ b/src/sequence_jacobian/utilities/forward_step.py @@ -211,4 +211,33 @@ def forward_step_cpol(D3, a_i, a_pi): d = D3[iw, iz, ia] D4[iw, iz, i] += d * pi D4[iw, iz, i+1] += d * (1 - pi) - return D4 \ No newline at end of file + return D4 + + +@njit +def forward_step_cpol_shock(D3, a_i_ss, a_pi_shock): + """forward_step_cpol linearized wrt a_pi""" + nS, nZ, nA = D3.shape + dD4 = np.zeros_like(D3) + for iw in range(nS): + for iz in range(nZ): + for ia in range(nA): + i = a_i_ss[iw, iz, ia] + dshock = a_pi_shock[iw, iz, ia] * D3[iw, iz, ia] + dD4[iw, iz, i] += dshock + dD4[iw, iz, i + 1] -= dshock + return dD4 + + +@njit +def forward_step_cpol_transpose(D3, a_i, a_pi): + """Transpose of forward_step_cpol""" + nS, nZ, nA = D3.shape + D4 = np.zeros_like(D3) + for iw in range(nS): + for iz in range(nZ): + for ia in range(nA): + i = a_i[iw, iz, ia] + pi = a_pi[iw, iz, ia] + D4[iw, iz, ia] = pi * D3[iw, iz, i] + (1 - pi) * D3[iw, iz, i + 1] + return D4 From 2d9703a53eea889c08033837c00df5665e453aa6 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 11:04:49 -0500 Subject: [PATCH 161/288] Computing steady state of nested CombinedBlock now works. --- src/sequence_jacobian/blocks/combined_block.py | 3 ++- src/sequence_jacobian/steady_state/drivers.py | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 3835b1f..5f8b662 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -9,6 +9,7 @@ from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict +from ..steady_state.classes import SteadyStateDict def combine(blocks, name="", model_alias=False): @@ -66,7 +67,7 @@ def steady_state(self, calibration, helper_blocks=None, **kwargs): ss_partial_eq = deepcopy(calibration) for i in topsorted: ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) - return ss_partial_eq + return SteadyStateDict(ss_partial_eq) def impulse_nonlinear(self, ss, exogenous, **kwargs): """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index dbe6e9c..e005c15 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -147,9 +147,14 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k if toplevel_unknowns is None: toplevel_unknowns = {} block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False + if dissolve is None: + dissolve = [] # Add the block's internal variables as inputs, if the block has an internal attribute - input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel + if isinstance(calibration, dict): + input_arg_dict = deepcopy(calibration) + else: + input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them # at the provided set of unknowns if included in dissolve. From 384b4151c77b50dac024d9cd0b6e47e07f7092c9 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 11:05:36 -0500 Subject: [PATCH 162/288] Testing DisContBlock: ss without helper works. --- dcblock.py | 447 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 dcblock.py diff --git a/dcblock.py b/dcblock.py new file mode 100644 index 0000000..9778928 --- /dev/null +++ b/dcblock.py @@ -0,0 +1,447 @@ +import numpy as np +from numba import guvectorize, njit +import dfols + +from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved +from sequence_jacobian.blocks.discont_block import discont +from sequence_jacobian.utilities.misc import choice_prob, logsum +from sequence_jacobian.steady_state.classes import SteadyStateDict + + +'''Core HA block''' + + +def household_init(a_grid, z_grid, b_grid, atw, transfer, rpost, eis, vphi, chi): + _, income = labor_income(z_grid, b_grid, atw, transfer, 0, 1, 1, 1) + coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + c_guess = 0.3 * coh + V, Va = np.empty_like(coh), np.empty_like(coh) + for iw in range(4): + V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi, chi) + V = V / 0.1 + + # get Va by finite difference + Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) + Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) + Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) + + return Va, V + + +@njit(fastmath=True) +def util(c, iw, eis, vphi, chi): + """Utility function.""" + # 1. utility from consumption. + if eis == 1: + u = np.log(c) + else: + u = c ** (1 - 1 / eis) / (1 - 1 / eis) + + # 2. disutility from work and search + if iw == 0: + u = u - vphi # E + elif (iw == 1) | (iw == 2): + u = u - chi # Ub, U + + return u + + +@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) +def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, z_grid, b_grid, lam_grid, eis, beta, rpost, + vphi, chi): + """ + Backward step function EGM with upper envelope. + + Dimensions: 0: labor market status, 1: productivity, 2: assets. + Status: 0: E, 1: Ub, 2: U, 3: O + State: 0: M, 1: B, 2: L + + Parameters + ---------- + V_p : array(Ns, Nz, Na), status-specific value function tomorrow + Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow + Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks + Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity + choice_set : list(Nz), discrete choices available in each state X + a_grid : array(Na), exogenous asset grid + y_grid : array(Ns, Nz), exogenous labor income grid + z_grid : array(Nz), productivity of employed (need for GE) + b_grid : array(Nz), productivity of unemployed (need for GE) + lam_grid : array(Nx), scale of taste shocks, specific to interim state + eis : float, EIS + beta : float, discount factor + rpost : float, ex-post interest rate + vphi : float, disutility of work + chi : float, disutility of search + + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function today + Va : array(Ns, Nz, Na), partial of status-specific value function today + P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x + c : array(Ns, Nz, Na), status-specific consumption policy today + a : array(Ns, Nz, Na), status-specific asset policy today + ze : array(Ns, Nz, Na), effective labor (average productivity if employed) + ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) + """ + # shapes + Ns, Nz, Na = V_p.shape + Nx = Pi_s_p.shape[1] + + # PART 1: update value and policy functions + # a. discrete choice I expect to make tomorrow + V_p_X = np.empty((Nx, Nz, Na)) + Va_p_X = np.empty((Nx, Nz, Na)) + for ix in range(Nx): + V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) + Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) + P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) + V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) + Va_p_X[ix, ...] = np.sum(P_p_ix * Va_p_ix, axis=0) + + # b. compute expectation wrt labor market shock + V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) + Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) + + # b. compute expectation wrt productivity + V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) + Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) + + # d. consumption today on tomorrow's grid and endogenous asset grid today + W = beta * V_p2 + uc_nextgrid = beta * Va_p2 + c_nextgrid = uc_nextgrid ** (-eis) + a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) + + # e. upper envelope + # V, c = upper_envelope(W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) + imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region + V, c = upper_envelope_fast(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) + + # f. update Va + uc = c ** (-1 / eis) + Va = (1 + rpost) * uc + + # PART 2: things we need for GE + + # 2/a. asset policy + a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c + + # 2/b. choice probabilities (don't need jacobian) + P = np.zeros((Nx, Ns, Nz, Na)) + for ix in range(Nx): + V_ix = np.take(V, indices=choice_set[ix], axis=0) + P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) + + # 2/c. average productivity of employed + ze = np.zeros_like(a) + ze[0, ...] = z_grid[:, np.newaxis] + + # 2/d. UI claims + ui = np.zeros_like(a) + ui[1, ...] = b_grid[:, np.newaxis] + + return V, Va, a, c, P, ze, ui + + +def labor_income(z_grid, b_grid, atw, transfer, expiry, fU, fN, s): + # 1. income + yE = atw * z_grid + transfer + yUb = atw * b_grid + transfer + yN = np.zeros_like(yE) + transfer + y_grid = np.vstack((yE, yUb, yN, yN)) + + # 2. transition matrix for labor market status + Pi_s = np.array([[1 - s, s, 0], [fU, (1 - fU) * (1 - expiry), (1 - fU) * expiry], [fU, 0, 1 - fU], [fN, 0, 1 - fN]]) + + return Pi_s, y_grid + + +household.add_hetinput(labor_income, verbose=False) + + +"""Supporting functions for HA block""" + + +@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) +def nonconcave(uc_nextgrid, imin, imax): + """Obtain bounds for non-concave region.""" + nA = uc_nextgrid.shape[-1] + vmin = np.inf + vmax = -np.inf + # step 1: find vmin & vmax + for ia in range(nA - 1): + if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: + vmin_temp = uc_nextgrid[ia] + vmax_temp = uc_nextgrid[ia + 1] + if vmin_temp < vmin: + vmin = vmin_temp + if vmax_temp > vmax: + vmax = vmax_temp + + # 2/a Find imin (upper bound) + if vmin == np.inf: + imin_ = 0 + else: + ia = 0 + while ia < nA: + if uc_nextgrid[ia] < vmin: + break + ia += 1 + imin_ = ia + + # 2/b Find imax (lower bound) + if vmax == -np.inf: + imax_ = nA + else: + ia = nA + while ia > 0: + if uc_nextgrid[ia] > vmax: + break + ia -= 1 + imax_ = ia + + imin[:] = imin_ + imax[:] = imax_ + + +@njit +def upper_envelope_fast(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): + """ + Interpolate consumption and value function to exogenous grid. Brute force but safe. + Parameters + ---------- + W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) + a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) + c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) + a_grid : array(Na), exogenous asset grid (tomorrow's grid) + y_grid : array(Ns, Nz), labor income + rpost : float, ex-post interest rate + args : (eis, vphi, chi) arguments for utility function + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function on exogenous grid + c : array(Ns, Nz, Na), consumption on exogenous grid + """ + + # 0. initialize + Ns, Nz, Na = W.shape + c = np.zeros_like(W) + V = -np.inf * np.ones_like(W) + + # outer loop could run in parallel + for iw in range(Ns): + for iz in range(Nz): + ycur = y_grid[iw, iz] + imaxcur = imax[iw, iz] + imincur = imin[iw, iz] + + # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. + # in concave region: exploit monotonicity and don't look for extra solutions + for ia in range(Na): + acur = a_grid[ia] + + # Region 1: below non-concave: exploit monotonicity + if (ia <= imaxcur) | (ia >= imincur): + iap = 0 + ap_low = a_nextgrid[iw, iz, iap] + ap_high = a_nextgrid[iw, iz, iap + 1] + while iap < Na - 2: # can this go up all the way? + if ap_high >= acur: + break + iap += 1 + ap_low = ap_high + ap_high = a_nextgrid[iw, iz, iap + 1] + # found bracket, interpolate value function and consumption + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess + c[iw, iz, ia] = c_guess + + # Region 2: non-concave region + else: + # try out all segments of endogenous grid + for iap in range(Na - 1): + # does this endogenous segment bracket ia? + ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] + interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) + + # does it need to be extrapolated above the endogenous grid? + # if needed to be extrapolated below, we would be in constrained case + extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) + + if interp or extrap_above: + # interpolation slopes + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + + # implied guess + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + + # value + v_guess = util(c_guess, iw, *args) + w_guess + + # select best value for this segment + if v_guess > V[iw, iz, ia]: + V[iw, iz, ia] = v_guess + c[iw, iz, ia] = c_guess + + # 2. constrained case: remember that we have the inverse asset policy a(a') + ia = 0 + while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: + c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur + V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] + ia += 1 + + return V, c + + +'''Simple blocks''' + + +@simple +def income_state_vars(mean_z, rho_z, sd_z, nZ, uirate, uicap): + # productivity + z_grid, pi_z, Pi_z = markov_rouwenhorst(rho=rho_z, sigma=sd_z, N=nZ) + z_grid *= mean_z + + # unemployment benefits + b_grid = uirate * z_grid + b_grid[b_grid > uicap] = uicap + return z_grid, b_grid, pi_z, Pi_z + + +@simple +def employment_state_vars(lamM, lamB, lamL): + choice_set = [[0, 3], [1, 3], [2, 3]] + lam_grid = np.array([lamM, lamB, lamL]) + return choice_set, lam_grid + + +@simple +def asset_state_vars(amin, amax, nA): + a_grid = agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +'''Rest of the model''' + + +@simple +def firm(Z, K, L, mc, alpha, delta, psi): + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + w = (1 - alpha) * mc * Y / L + u = 1.0 + Q = 1 + psi * (K / K(-1) - 1) + I = K - (1 - delta) * K(-1) + psi / 2 * (K / K(-1) - 1) ** 2 * K(-1) + transfer = Y - w * L - I + return Y, w, u, Q, I, transfer + + +@simple +def monetary(pi, rstar, phi_pi): + # rpost = (1 + rstar(-1) + phi_pi * pi(-1)) / (1 + pi) - 1 + rpost = rstar + return rpost + + +@simple +def nkpc(pi, mc, eps, Y, rpost, kappa): + nkpc_res = kappa * (mc - (eps - 1) / eps) + Y(+1) / Y * np.log(1 + pi(+1)) / (1 + rpost(+1)) - np.log(1 + pi) + return nkpc_res + + +@simple +def valuation(rpost, mc, Y, K, Q, delta, psi, alpha): + val = alpha * mc(+1) * Y(+1) / K - (K(+1) / K - (1 - delta) + psi / 2 * (K(+1) / K - 1) ** 2) + \ + K(+1) / K * Q(+1) - (1 + rpost(+1)) * Q + return val + + +@solved(unknowns={'B': [0.0, 10.0]}, targets=['budget'], solver='brentq') +def fiscal(B, tax, w, rpost, G, Ze, Ui): + budget = (1 + rpost) * B + G + (1 - tax) * w * Ui - tax * w * Ze - B + # tax_rule = tax - tax.ss - phi * (B(-1) - B.ss) / Y.ss + tax_rule = B - B.ss + return budget, tax_rule + + +@simple +def fiscal2(w, tax): + atw = (1 - tax) * w + return atw + + +@solved(unknowns={'fU': 0.25, 'fN': 0.1, 's': 0.025}, targets=['fU_res', 'fN_res', 's_res'], solver='broyden_custom') +def flows(Y, fU, fN, s, fU_eps, fN_eps, s_eps): + fU_res = fU.ss * (Y / Y.ss) ** fU_eps - fU + fN_res = fN.ss * (Y / Y.ss) ** fN_eps - fN + s_res = s.ss * (Y / Y.ss) ** s_eps - s + return fU_res, fN_res, s_res + + +@simple +def mkt_clearing(A, B, Y, C, I, G, L, Ze): + asset_mkt = A - B + goods_mkt = Y - C - I - G + labor_mkt = L - Ze + return asset_mkt, goods_mkt, labor_mkt + + +@simple +def partial_ss(Y, Ze, rpost, alpha, delta, eps): + # uses u=1 + L = Ze + mc = (eps - 1) / eps + K = mc * Y * alpha / (rpost + delta) + # transfer = rpost * K + Y * (1 - mc) + + # depend on L + Z = Y / (K ** alpha * L ** (1 - alpha)) + w = (1 - alpha) * mc * Y / L + # atw = (1 - tax) * w + I = delta * K + Q = 1.0 + return mc, K, Z, w, I, Q + + +'''Try this''' + + +hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, household], name='SingleMen') + +# calibration = {'beta': 0.99, 'eis': 1.0, 'vphi': 0.65, 'chi': 0.6, +# 'rpost': 0.002, 'uicap': 0.66, 'uirate': 0.5, 'atw': 0.9, 'transfer': 0.15, +# 'expiry': 1/6, 'fU': 0.25, 'fN': 0.1, 's': 0.025, +# 'amin': 0.0, 'amax': 500, 'nA': 100, +# 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, +# 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7} +# +# hh_ss = hh.solve_steady_state(calibration, solver='hybr', unknowns={'mean_z': 1.2}, targets={'Ze': 1.0}) + +# J = household.jacobian(ss, exogenous=['beta', 'fU'], T=10) + +calibration = {'eis': 1.0, 'vphi': 0.65, 'chi': 0.6, + 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'fU': 0.25, 'fN': 0.1, 's': 0.025, + 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, + 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7, + 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0, 'alpha': 0.2, 'psi': 30, 'delta': 0.0083, + 'eps': 10, 'fU_eps': 10.0, 'fN_eps': 5.0, 's_eps': -8.0, 'tax': 0.3, 'B': 3.0} + +hank = create_model([hh, firm, monetary, valuation, nkpc, fiscal, fiscal2, flows, mkt_clearing], name='HANK') + +ss = hank.solve_steady_state(calibration, solver='hybr', dissolve=[fiscal, flows], + unknowns={'Z': 1.0, 'L': 1.0, 'mc': 0.9, 'G': 0.3, + 'beta': 0.99, 'K': 6.0}, + targets={'Y': 1.0, 'labor_mkt': 0.0, 'nkpc_res': 0.0, 'budget': 0.0, + 'asset_mkt': 0.0, 'val': 0.0}) + + + From 13b7f2ba8ba4b57cfbd0577b1e54b28612e5833c Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 11:30:35 -0500 Subject: [PATCH 163/288] Testing DisContBlock: ss with helper works. --- dcblock.py | 64 +++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/dcblock.py b/dcblock.py index 9778928..5cc87b1 100644 --- a/dcblock.py +++ b/dcblock.py @@ -335,14 +335,16 @@ def asset_state_vars(amin, amax, nA): @simple -def firm(Z, K, L, mc, alpha, delta, psi): +def firm(Z, K, L, mc, alpha, delta0, delta1, psi): Y = Z * K(-1) ** alpha * L ** (1 - alpha) w = (1 - alpha) * mc * Y / L - u = 1.0 + u = (alpha / delta0 / delta1 * mc * Y / K(-1)) ** (1 / delta1) + # u = 1.0 + delta = delta0 * u ** delta1 Q = 1 + psi * (K / K(-1) - 1) I = K - (1 - delta) * K(-1) + psi / 2 * (K / K(-1) - 1) ** 2 * K(-1) transfer = Y - w * L - I - return Y, w, u, Q, I, transfer + return Y, w, u, delta, Q, I, transfer @simple @@ -366,7 +368,7 @@ def valuation(rpost, mc, Y, K, Q, delta, psi, alpha): @solved(unknowns={'B': [0.0, 10.0]}, targets=['budget'], solver='brentq') -def fiscal(B, tax, w, rpost, G, Ze, Ui): +def fiscal1(B, tax, w, rpost, G, Ze, Ui): budget = (1 + rpost) * B + G + (1 - tax) * w * Ui - tax * w * Ze - B # tax_rule = tax - tax.ss - phi * (B(-1) - B.ss) / Y.ss tax_rule = B - B.ss @@ -396,52 +398,44 @@ def mkt_clearing(A, B, Y, C, I, G, L, Ze): @simple -def partial_ss(Y, Ze, rpost, alpha, delta, eps): +def helper1(A, tax, w, Ze, rpost, Ui): + # after hh + B = A + G = tax * w * Ze - rpost * B - (1 - tax) * w * Ui + return B, G + + +@simple +def helper2(eps, rpost, Y, L, alpha, delta0): # uses u=1 - L = Ze mc = (eps - 1) / eps - K = mc * Y * alpha / (rpost + delta) - # transfer = rpost * K + Y * (1 - mc) - - # depend on L + K = mc * Y * alpha / (rpost + delta0) + delta1 = alpha / delta0 * mc * Y / K Z = Y / (K ** alpha * L ** (1 - alpha)) w = (1 - alpha) * mc * Y / L - # atw = (1 - tax) * w - I = delta * K - Q = 1.0 - return mc, K, Z, w, I, Q + return mc, K, delta1, Z, w '''Try this''' -hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, household], name='SingleMen') - -# calibration = {'beta': 0.99, 'eis': 1.0, 'vphi': 0.65, 'chi': 0.6, -# 'rpost': 0.002, 'uicap': 0.66, 'uirate': 0.5, 'atw': 0.9, 'transfer': 0.15, -# 'expiry': 1/6, 'fU': 0.25, 'fN': 0.1, 's': 0.025, -# 'amin': 0.0, 'amax': 500, 'nA': 100, -# 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, -# 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7} -# -# hh_ss = hh.solve_steady_state(calibration, solver='hybr', unknowns={'mean_z': 1.2}, targets={'Ze': 1.0}) - -# J = household.jacobian(ss, exogenous=['beta', 'fU'], T=10) - calibration = {'eis': 1.0, 'vphi': 0.65, 'chi': 0.6, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'fU': 0.25, 'fN': 0.1, 's': 0.025, 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, - 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7, - 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0, 'alpha': 0.2, 'psi': 30, 'delta': 0.0083, - 'eps': 10, 'fU_eps': 10.0, 'fN_eps': 5.0, 's_eps': -8.0, 'tax': 0.3, 'B': 3.0} + 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7, 'beta': 0.985, + 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083, + 'eps': 10, 'fU_eps': 10.0, 'fN_eps': 5.0, 's_eps': -8.0, 'tax': 0.3} -hank = create_model([hh, firm, monetary, valuation, nkpc, fiscal, fiscal2, flows, mkt_clearing], name='HANK') +hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, household], name='SingleMen') +hank = create_model([hh, firm, monetary, valuation, nkpc, fiscal1, fiscal2, flows, mkt_clearing], name='HANK') -ss = hank.solve_steady_state(calibration, solver='hybr', dissolve=[fiscal, flows], - unknowns={'Z': 1.0, 'L': 1.0, 'mc': 0.9, 'G': 0.3, - 'beta': 0.99, 'K': 6.0}, +ss = hank.solve_steady_state(calibration, solver='hybr', dissolve=[fiscal1, flows], + unknowns={'Z': 0.63, 'L': 0.86, 'mc': 0.9, 'G': 0.2, 'delta1': 1.2, + 'B': 3.0, 'K': 17.0}, targets={'Y': 1.0, 'labor_mkt': 0.0, 'nkpc_res': 0.0, 'budget': 0.0, - 'asset_mkt': 0.0, 'val': 0.0}) + 'asset_mkt': 0.0, 'val': 0.0, 'u': 1.0}, + helper_blocks=[helper1, helper2], + helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) From e0ef2a3cae19a0078bf4b5a0b9e0059284511b2f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 13:16:40 -0500 Subject: [PATCH 164/288] Bijection class from @mrognlie and basic tests. --- .../blocks/support/bijection.py | 44 +++++++++++++++++++ tests/base/test_public_classes.py | 15 +++++++ 2 files changed, 59 insertions(+) create mode 100644 src/sequence_jacobian/blocks/support/bijection.py diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py new file mode 100644 index 0000000..38d1b39 --- /dev/null +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -0,0 +1,44 @@ +class Bijection: + def __init__(self, map): + # identity always implicit, remove if there explicitly + self.map = {k: v for k, v in map.items() if k != v} + invmap = {} + for k, v in map.items(): + if v in invmap: + raise ValueError(f'Duplicate value {v}, for keys {invmap[v]} and {k}') + invmap[v] = k + self.invmap = invmap + + @property + def inv(self): + invmap = Bijection.__new__(Bijection) # better way to do this? + invmap.map = self.invmap + invmap.invmap = self.map + return invmap + + def __repr__(self): + return f'Bijection({repr(self.map)})' + + def __getitem__(self, k): + return self.map.get(k, k) + + def __matmul__(self, x): + if isinstance(x, Bijection): + # compose self: v -> u with x: w -> v + # assume everything missing in either is the identity + M = {} + for v, u in self.map.items(): + w = x.invmap.get(v, v) + M[w] = u + for w, v in x.map.items(): + if v not in self.map: + M[w] = v + return Bijection(M) + elif isinstance(x, dict): + return {self[k]: v for k, v in x.items()} + elif isinstance(x, list): + return [self[k] for k in x] + elif isinstance(x, tuple): + return tuple(self[k] for k in x) + else: + return NotImplementedError diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 57a86e8..3383393 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -1,10 +1,12 @@ """Test public-facing classes""" import numpy as np +import pytest from sequence_jacobian import het from sequence_jacobian.steady_state.classes import SteadyStateDict from sequence_jacobian.blocks.support.impulse import ImpulseDict +from sequence_jacobian.blocks.support.bijection import Bijection def test_steadystatedict(): @@ -72,3 +74,16 @@ def test_impulsedict(krusell_smith_dag): dC1 = 100 * ir_lin['C'] / ss['C'] dC2 = 100 * ir_lin[['C']] / ss assert np.allclose(dC1, dC2['C']) + + +def test_bijection(): + mymap = Bijection({'a1': 'a', 'b1': 'b'}) + mymapinv = mymap.inv + + assert mymap['a1'] == 'a' and mymap['b1'] == 'b' + assert mymapinv['a'] == 'a1' and mymapinv['b'] == 'b1' + + with pytest.raises(ValueError): + badmap = Bijection({'a1': 'a', 'b1': 'a'}) + + From c0fea5c6c3b0e9fa9a21563fac61e38018c8c862 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 13:29:56 -0500 Subject: [PATCH 165/288] Bijection classL test composition as well. --- tests/base/test_public_classes.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 3383393..2d0d0e6 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -77,13 +77,16 @@ def test_impulsedict(krusell_smith_dag): def test_bijection(): - mymap = Bijection({'a1': 'a', 'b1': 'b'}) + # generate and invert + mymap = Bijection({'a': 'a1', 'b': 'b1'}) mymapinv = mymap.inv + assert mymap['a'] == 'a1' and mymap['b'] == 'b1' + assert mymapinv['a1'] == 'a' and mymapinv['b1'] == 'b' - assert mymap['a1'] == 'a' and mymap['b1'] == 'b' - assert mymapinv['a'] == 'a1' and mymapinv['b'] == 'b1' - + # duplicate keys rejected with pytest.raises(ValueError): - badmap = Bijection({'a1': 'a', 'b1': 'a'}) - + Bijection({'a': 'a1', 'b': 'a1'}) + # composition + mymap2 = Bijection({'A': 'a'}) + assert (mymap @ mymap2)['A'] == 'a1' From a5029df0fce7dbe2e5bc0ecc628091c310e6f6e7 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 15 Jun 2021 14:26:01 -0500 Subject: [PATCH 166/288] Blocks have `.M` attribute that stores the bijection membrane. --- src/sequence_jacobian/blocks/discont_block.py | 1 + src/sequence_jacobian/blocks/het_block.py | 2 ++ src/sequence_jacobian/blocks/simple_block.py | 2 ++ src/sequence_jacobian/primitives.py | 7 +++++++ 4 files changed, 12 insertions(+) diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index f0e3873..b2888a3 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -56,6 +56,7 @@ def __init__(self, back_step_fun, exogenous, policy, disc_policy, backward, back Currently, we only support up to two policy variables. """ self.name = back_step_fun.__name__ + self.M = Bijection({}) # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 259d4d8..11b1c4d 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -7,6 +7,7 @@ from .. import utilities as utils from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict +from .support.bijection import Bijection from ..devtools.deprecate import rename_output_list_to_outputs from ..utilities.misc import verify_saved_jacobian @@ -55,6 +56,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non Currently, we only support up to two policy variables. """ self.name = back_step_fun.__name__ + self.M = Bijection({}) # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index df43848..1f8cf16 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -6,6 +6,7 @@ from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .support.impulse import ImpulseDict +from .support.bijection import Bijection from ..primitives import Block from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix @@ -40,6 +41,7 @@ def __init__(self, f): self.output_list = misc.output_list(f) self.inputs = set(self.input_list) self.outputs = set(self.output_list) + self.M = Bijection({}) def __repr__(self): return f"" diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 15a2def..6ea2095 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -4,6 +4,7 @@ from abc import ABCMeta as NativeABCMeta from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List +from copy import deepcopy from .steady_state.drivers import steady_state from .steady_state.support import provide_solver_default @@ -12,6 +13,7 @@ from .steady_state.classes import SteadyStateDict from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict +from .blocks.support.bijection import Bijection # Basic types Array = Any @@ -135,3 +137,8 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], variables to be solved for and the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) + + def remap(self, map): + remapped = deepcopy(self) + remapped.M = self.M @ Bijection(map) + return remapped From c9d31e77d7264b3cb251fd2ae3bf07794f33a6d2 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 09:22:58 -0500 Subject: [PATCH 167/288] Fixed rogue block.name in steady state solver. --- src/sequence_jacobian/steady_state/drivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index e005c15..4f7f154 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -163,7 +163,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k if block in dissolve and "solver" in valid_input_kwargs: input_kwarg_dict["solver"] = "solved" input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} - elif block.name not in dissolve and block_unknowns_in_toplevel_unknowns: + elif block not in dissolve and block_unknowns_in_toplevel_unknowns: raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," f" {set(toplevel_unknowns)}.\n" From 681c2644b3f520e3cde22d565b0d46e4df77481f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 09:59:51 -0500 Subject: [PATCH 168/288] Removing deprecated .ss, .jac, .td methods in progress. --- src/sequence_jacobian/blocks/discont_block.py | 12 ++++---- src/sequence_jacobian/blocks/het_block.py | 28 ++++------------- src/sequence_jacobian/blocks/simple_block.py | 18 ----------- src/sequence_jacobian/blocks/solved_block.py | 18 ----------- src/sequence_jacobian/models/krusell_smith.py | 22 +++++++++----- tests/base/test_jacobian.py | 30 +++++++++---------- tests/base/test_simple_block.py | 10 +++---- 7 files changed, 46 insertions(+), 92 deletions(-) diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index b2888a3..a9e4402 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -157,14 +157,14 @@ def __repr__(self): return f"" '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - - ss : do backward and forward iteration until convergence to get complete steady state - - td : do backward and forward iteration up to T to compute dynamics given some shocks - - jac : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - - ajac : compute asymptotic columns of jacobians output by jac, also using fake news algorithm + - steady_state : do backward and forward iteration until convergence to get complete steady state + - impulse_nonlinear : do backward and forward iteration up to T to compute dynamics given some shocks + - impulse_linear : apply jacobians to compute linearized dynamics given some shocks + - jacobian : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after - each backward iteration step in td + each backward iteration step in td, jacobian is not computed for these! ''' @@ -771,7 +771,7 @@ def J_from_F(F): J[1:, t] += J[:-1, t - 1] return J - '''Part 5: helpers for .jac and .ajac: preliminary processing''' + '''Part 5: helpers for .jac: preliminary processing''' def jac_prelim(self, ss): """Helper that does preliminary processing of steady state for fake news algorithm. diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 11b1c4d..bb76e11 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -155,34 +155,16 @@ def __repr__(self): return f"" '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - - ss : do backward and forward iteration until convergence to get complete steady state - - td : do backward and forward iteration up to T to compute dynamics given some shocks - - jac : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - - ajac : compute asymptotic columns of jacobians output by jac, also using fake news algorithm + - steady_state : do backward and forward iteration until convergence to get complete steady state + - impulse_nonlinear : do backward and forward iteration up to T to compute dynamics given some shocks + - impulse_linear : apply jacobians to compute linearized dynamics given some shocks + - jacobian : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after - each backward iteration step in td + each backward iteration step in td, jacobian is not computed for these! ''' - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, **kwargs) - - def jac(self, ss, shock_list=None, T=None, **kwargs): - if shock_list is None: - shock_list = list(self.inputs) - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 1f8cf16..25c28cc 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -46,24 +46,6 @@ def __init__(self, f): def __repr__(self): return f"" - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, exogenous=kwargs) - - def jac(self, ss, T=None, shock_list=None): - if shock_list is None: - shock_list = [] - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, exogenous=shock_list, T=T) - def steady_state(self, calibration): input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 3a2caa7..7f37cea 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -66,24 +66,6 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ def __repr__(self): return f"" - # TODO: Deprecated methods, to be removed! - def ss(self, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .steady_state", DeprecationWarning) - return self.steady_state(kwargs) - - def td(self, ss, **kwargs): - warnings.warn("This method has been deprecated. Please invoke by calling .impulse_nonlinear", - DeprecationWarning) - return self.impulse_nonlinear(ss, **kwargs) - - def jac(self, ss, T=None, shock_list=None, **kwargs): - if shock_list is None: - shock_list = [] - warnings.warn("This method has been deprecated. Please invoke by calling .jacobian.\n" - "Also, note that the kwarg `shock_list` in .jacobian has been renamed to `shocked_vars`", - DeprecationWarning) - return self.jacobian(ss, shock_list, T, **kwargs) - def steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py index 82e4559..7058661 100644 --- a/src/sequence_jacobian/models/krusell_smith.py +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -4,6 +4,7 @@ from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.het_block import het +from ..steady_state.classes import SteadyStateDict '''Part 1: HA block''' @@ -105,21 +106,28 @@ def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) # figure out initializer - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + # coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + # Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + calibration = {'Pi': Pi, 'a_grid': a_grid, 'e_grid': e_grid, 'r': r, 'w': w, 'eis': eis} + calibration = SteadyStateDict(calibration) # solve for beta consistent with this beta_min = lb / (1 + r) beta_max = ub / (1 + r) - beta, sol = opt.brentq(lambda bet: household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=bet, eis=eis, - Va=Va)['A'] - K, beta_min, beta_max, full_output=True) + def res(beta_): + calibration['beta'] = beta_ + return household.steady_state(calibration)['A'] - K + + beta, sol = opt.brentq(res, beta_min, beta_max, full_output=True) + if not sol.converged: raise ValueError('Steady-state solver did not converge.') # extra evaluation to report variables - ss = household.ss(Pi=Pi, a_grid=a_grid, e_grid=e_grid, r=r, w=w, beta=beta, eis=eis, Va=Va) - ss.update({'Pi': Pi, 'Z': Z, 'K': K, 'L': 1, 'Y': Y, 'alpha': alpha, 'delta': delta, + calibration['beta'] = beta + ss = household.steady_state(calibration) + ss.update({'Z': Z, 'K': K, 'L': 1.0, 'Y': Y, 'alpha': alpha, 'delta': delta, 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, - 'rho': rho, 'nS': nS, 'asset_mkt': ss["A"] - K}) + 'rho': rho, 'nS': nS, 'asset_mkt': ss['A'] - K}) return ss diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index e097bb9..431b36d 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -15,8 +15,8 @@ def test_ks_jac(krusell_smith_dag): G2 = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) # Manually calculate the general equilibrium Jacobian - J_firm = firm.jac(ss, shock_list=['K', 'Z']) - J_ha = household.jac(ss, T=T, shock_list=['r', 'w']) + J_firm = firm.jacobian(ss, exogenous=['K', 'Z']) + J_ha = household.jacobian(ss, T=T, exogenous=['r', 'w']) J_curlyK_K = J_ha['A']['r'] @ J_firm['r']['K'] + J_ha['A']['w'] @ J_firm['w']['K'] J_curlyK_Z = J_ha['A']['r'] @ J_firm['r']['Z'] + J_ha['A']['w'] @ J_firm['w']['Z'] J_curlyK = {'curlyK': {'K': J_curlyK_K, 'Z': J_curlyK_Z}} @@ -62,8 +62,8 @@ def test_fake_news_v_actual(one_asset_hank_dag): household = hank_model._blocks_unsorted[0] T = 40 - shock_list = ['w', 'r', 'Div', 'Tax'] - Js = household.jac(ss, shock_list, T) + exogenous = ['w', 'r', 'Div', 'Tax'] + Js = household.jacobian(ss, exogenous, T) output_list = household.non_back_iter_outputs # Preliminary processing of the steady state @@ -72,7 +72,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): # Step 1 of fake news algorithm: backward iteration h = 1E-4 curlyYs, curlyDs = {}, {} - for i in shock_list: + for i in exogenous: curlyYs[i], curlyDs[i] = household.backward_iteration_fakenews(i, output_list, ssin_dict, ssout_list, ss.internal["household"]['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, @@ -94,7 +94,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): # Step 3 of fake news algorithm: combine everything to make the fake news matrix for each output-input pair Fs = {o.capitalize(): {} for o in output_list} for o in output_list: - for i in shock_list: + for i in exogenous: F = np.empty((T,T)) F[0, ...] = curlyYs[i][o] F[1:, ...] = curlyPs[o].reshape(T-1, -1) @ curlyDs[i].reshape(T, -1).T @@ -109,7 +109,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): Js_original = Js Js = {o.capitalize(): {} for o in output_list} for o in output_list: - for i in shock_list: + for i in exogenous: # implement recursion (30): start with J=F and accumulate terms along diagonal J = Fs[o.capitalize()][i].copy() for t in range(1, J.shape[1]): @@ -117,7 +117,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): Js[o.capitalize()][i] = J for o in output_list: - for i in shock_list: + for i in exogenous: assert np.array_equal(Js[o.capitalize()][i], Js_original[o.capitalize()][i]) @@ -126,23 +126,23 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): household = hank_model._blocks_unsorted[0] T = 40 - shock_list = 'r' + exogenous = 'r' output_list = household.non_back_iter_outputs h = 1E-4 - Js = household.jac(ss, shock_list, T) - Js_direct = {o.capitalize(): {i: np.empty((T, T)) for i in shock_list} for o in output_list} + Js = household.jacobian(ss, exogenous, T) + Js_direct = {o.capitalize(): {i: np.empty((T, T)) for i in exogenous} for o in output_list} # run td once without any shocks to get paths to subtract against # (better than subtracting by ss since ss not exact) # monotonic=True lets us know there is monotonicity of policy rule, makes TD run faster - # .td requires at least one input 'shock', so we put in steady-state w - td_noshock = household.td(ss, exogenous={"w": np.zeros(T)}, monotonic=True) + # .impulse_nonlinear requires at least one input 'shock', so we put in steady-state w + td_noshock = household.impulse_nonlinear(ss, exogenous={'w': np.zeros(T)}, monotonic=True) - for i in shock_list: + for i in exogenous: # simulate with respect to a shock at each date up to T for t in range(T): - td_out = household.td(ss, exogenous={i: h * (np.arange(T) == t)}) + td_out = household.impulse_nonlinear(ss, exogenous={i: h * (np.arange(T) == t)}) # store results as column t of J[o][i] for each outcome o for o in output_list: diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 6610dba..9c6fda4 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -38,15 +38,15 @@ def test_block_consistency(block, ss): """Make sure ss, td, and jac methods are all consistent with each other. Requires that all inputs of simple block allow calculating Jacobians""" # get ss output - ss_results = block.ss(**ss) + ss_results = block.steady_state(ss) # now if we put in constant inputs, td should give us the same! - td_results = block.td(ss_results, **{k: np.zeros(20) for k in ss.keys()}) + td_results = block.impulse_nonlinear(ss_results, exogenous={k: np.zeros(20) for k in ss.keys()}) for k, v in td_results.impulse.items(): assert np.all(v == 0) # now get the Jacobian - J = block.jac(ss, shock_list=block.input_list) + J = block.jacobian(ss, exogenous=block.input_list) # now perturb the steady state by small random vectors # and verify that the second-order numerical derivative implied by .td @@ -54,8 +54,8 @@ def test_block_consistency(block, ss): h = 1E-5 all_shocks = {i: np.random.rand(10) for i in block.input_list} - td_up = block.td(ss_results, **{i: h*shock for i, shock in all_shocks.items()}) - td_dn = block.td(ss_results, **{i: -h*shock for i, shock in all_shocks.items()}) + td_up = block.impulse_nonlinear(ss_results, exogenous={i: h*shock for i, shock in all_shocks.items()}) + td_dn = block.impulse_nonlinear(ss_results, exogenous={i: -h*shock for i, shock in all_shocks.items()}) linear_impulses = {o: (td_up.impulse[o] - td_dn.impulse[o])/(2*h) for o in td_up.impulse} linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up.impulse} From 18f4527038611bd0bbe04be518ca16786b23f14d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 11:41:07 -0500 Subject: [PATCH 169/288] Deprecated .ss, .jac, .td methods removed. All tests pass. --- .../blocks/support/bijection.py | 4 ++-- src/sequence_jacobian/models/hank.py | 15 ++++++++------- src/sequence_jacobian/models/krusell_smith.py | 12 ++++-------- src/sequence_jacobian/models/two_asset.py | 17 +++++++++-------- tests/base/test_two_asset.py | 10 ++++------ 5 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index 38d1b39..b8bf815 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -40,5 +40,5 @@ def __matmul__(self, x): return [self[k] for k in x] elif isinstance(x, tuple): return tuple(self[k] for k in x) - else: - return NotImplementedError + # else: + # return NotImplementedError diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py index 8ee9576..a38df0e 100644 --- a/src/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -190,9 +190,8 @@ def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1 Tax = r * B T = transfers(pi_e, Div, Tax, e_grid) - # initialize guess for policy function iteration - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + calibration = {'Pi': Pi, 'a_grid': a_grid, 'e_grid': e_grid, 'pi_e': pi_e, 'w': w, 'r': r, 'eis': eis, 'Div': Div, + 'Tax': Tax, 'frisch': frisch} # residual function def res(x): @@ -200,16 +199,18 @@ def res(x): # precompute constrained c and n which don't depend on Va if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001: raise ValueError('Clearly invalid inputs') - out = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta_loc, - eis=eis, Div=Div, Tax=Tax, frisch=frisch, vphi=vphi_loc) + + calibration['beta'], calibration['vphi'] = beta_loc, vphi_loc + out = household.steady_state(calibration) + return np.array([out['A'] - B, out['N_e'] - 1]) # solve for beta, vphi (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) + calibration['beta'], calibration['vphi'] = beta, vphi # extra evaluation for reporting - ss = household.ss(Va=Va, Pi=Pi, a_grid=a_grid, e_grid=e_grid, pi_e=pi_e, w=w, r=r, beta=beta, eis=eis, - Div=Div, Tax=Tax, frisch=frisch, vphi=vphi) + ss = household.steady_state(calibration) # check Walras's law goods_mkt = 1 - ss['C'] diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py index 7058661..378664f 100644 --- a/src/sequence_jacobian/models/krusell_smith.py +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -105,28 +105,24 @@ def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, Y = Z * K ** alpha w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - # figure out initializer - # coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - # Va = (1 + r) * (0.1 * coh) ** (-1 / eis) calibration = {'Pi': Pi, 'a_grid': a_grid, 'e_grid': e_grid, 'r': r, 'w': w, 'eis': eis} - calibration = SteadyStateDict(calibration) # solve for beta consistent with this beta_min = lb / (1 + r) beta_max = ub / (1 + r) - def res(beta_): - calibration['beta'] = beta_ + def res(beta_loc): + calibration['beta'] = beta_loc return household.steady_state(calibration)['A'] - K beta, sol = opt.brentq(res, beta_min, beta_max, full_output=True) + calibration['beta'] = beta if not sol.converged: raise ValueError('Steady-state solver did not converge.') # extra evaluation to report variables - calibration['beta'] = beta ss = household.steady_state(calibration) - ss.update({'Z': Z, 'K': K, 'L': 1.0, 'Y': Y, 'alpha': alpha, 'delta': delta, + ss.update({'Z': Z, 'K': K, 'L': 1.0, 'Y': Y, 'alpha': alpha, 'delta': delta, 'Pi': Pi, 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, 'rho': rho, 'nS': nS, 'asset_mkt': ss['A'] - K}) diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 403bdbe..9948f1c 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -359,26 +359,27 @@ def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10 rb = r - omega # figure out initializer - z_grid = income(e_grid, tax, w, 1) - Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + calibration = {'Pi': Pi, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, + 'N': 1.0, 'tax': tax, 'w': w, 'eis': eis, 'rb': rb, 'ra': ra, 'chi0': chi0, 'chi2': chi2} # residual function def res(x): beta_loc, chi1_loc = x + if beta_loc > 0.999 / (1 + r) or chi1_loc < 0.5: raise ValueError('Clearly invalid inputs') - out = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, - k_grid=k_grid, beta=beta_loc, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1_loc, chi2=chi2) + + calibration['beta'], calibration['chi1'] = beta_loc, chi1_loc + out = household.steady_state(calibration) asset_mkt = out['A'] + out['B'] - p - Bg return np.array([asset_mkt, out['B'] - Bh]) # solve for beta, vphi, omega (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), verbose=verbose) + calibration['beta'], calibration['chi1'] = beta, chi1 - # extra evaluation to report variables - ss = household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, N=1, tax=tax, w=w, e_grid=e_grid, - k_grid=k_grid, beta=beta, eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1, chi2=chi2) + # extra evaluation for reporting + ss = household.steady_state(calibration) # other things of interest vphi = (1 - tax) * w * ss['U'] / muw diff --git a/tests/base/test_two_asset.py b/tests/base/test_two_asset.py index 1454e5c..94ce995 100644 --- a/tests/base/test_two_asset.py +++ b/tests/base/test_two_asset.py @@ -36,13 +36,11 @@ def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg= rb = r - omega # figure out initializer - z_grid = two_asset.income(e_grid, tax, w, 1) - Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + calibration = {'Pi': Pi, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, + 'beta': beta, 'N': 1.0, 'tax': tax, 'w': w, 'eis': eis, 'rb': rb, 'ra': ra, + 'chi0': chi0, 'chi1': chi1, 'chi2': chi2} - out = two_asset.household.ss(Va=Va, Vb=Vb, Pi=Pi, a_grid=a_grid, b_grid=b_grid, - N=1, tax=tax, w=w, e_grid=e_grid, k_grid=k_grid, beta=beta, - eis=eis, rb=rb, ra=ra, chi0=chi0, chi1=chi1, chi2=chi2) + out = two_asset.household.steady_state(calibration) return out['A'], out['B'], out['U'] From f04a13ff4a7c432085630463eb1fdabe1b8bc3cd Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 12:08:58 -0500 Subject: [PATCH 170/288] Left multiplication (SteadyStateDict @ Bijection) works with test. --- src/sequence_jacobian/primitives.py | 2 ++ src/sequence_jacobian/steady_state/classes.py | 11 +++++++++++ tests/base/test_public_classes.py | 12 +++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 6ea2095..c4a13a5 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -75,6 +75,8 @@ def outputs(self): def steady_state(self, calibration: Dict[str, Union[Real, Array]], **kwargs) -> SteadyStateDict: raise NotImplementedError(f'{type(self)} does not implement .steady_state()') + # def steady_state(self, calibration, **kwargs): + # return self.M @ self.steady_state(self.M.inv @ calibration, **kwargs) def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index cb8238f..e4ff000 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -3,6 +3,7 @@ from copy import deepcopy from ..utilities.misc import dict_diff +from ..blocks.support.bijection import Bijection class SteadyStateDict: @@ -32,6 +33,16 @@ def __getitem__(self, k): def __setitem__(self, k, v): self.toplevel[k] = v + def __matmul__(self, x): + # remap keys in toplevel + if isinstance(x, Bijection): + new = deepcopy(self) + new.toplevel = x @ self.toplevel + return new + + def __rmatmul__(self, x): + return self.__matmul__(x) + def keys(self): return self.toplevel.keys() diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 2d0d0e6..1c325e8 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -87,6 +87,12 @@ def test_bijection(): with pytest.raises(ValueError): Bijection({'a': 'a1', 'b': 'a1'}) - # composition - mymap2 = Bijection({'A': 'a'}) - assert (mymap @ mymap2)['A'] == 'a1' + # composition with another bijection (flows backwards) + mymap2 = Bijection({'a1': 'a2'}) + assert (mymap2 @ mymap)['a'] == 'a2' + + # composition with SteadyStateDict (only left __matmul__ works as intended) + ss = SteadyStateDict({'a': 2.0, 'b': 1.0}, internal={}) + ss_remapped = ss @ mymap + assert isinstance(ss_remapped, SteadyStateDict) + assert ss_remapped['a1'] == ss['a'] and ss_remapped['b1'] == ss['b'] From 2d81597b4c1c1ba88112eb2df5ac96aa1fd9adfd Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 14:51:26 -0500 Subject: [PATCH 171/288] Remap: .steady_state and .solve_steady_state work with some caveats. 1. Repeated blocks (low and high type) won't work out of the gate. 2. Noticed that dissolve does not work properly. Investigate separately. --- src/sequence_jacobian/blocks/combined_block.py | 4 +++- src/sequence_jacobian/blocks/discont_block.py | 5 +++-- src/sequence_jacobian/blocks/het_block.py | 7 ++++--- src/sequence_jacobian/blocks/simple_block.py | 2 +- src/sequence_jacobian/blocks/solved_block.py | 6 ++++-- src/sequence_jacobian/blocks/support/bijection.py | 9 +++++++++ src/sequence_jacobian/primitives.py | 14 +++++++------- src/sequence_jacobian/steady_state/drivers.py | 9 +++++++-- 8 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 5f8b662..1cbf863 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -10,6 +10,7 @@ from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict from ..steady_state.classes import SteadyStateDict +from .support.bijection import Bijection def combine(blocks, name="", model_alias=False): @@ -34,6 +35,7 @@ def __init__(self, blocks, name="", model_alias=False): self._sorted_indices = utils.graph.block_sort(blocks) self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] + self.M = Bijection({}) if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" @@ -56,7 +58,7 @@ def __repr__(self): else: return f"" - def steady_state(self, calibration, helper_blocks=None, **kwargs): + def _steady_state(self, calibration, helper_blocks=None, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" if helper_blocks is None: helper_blocks = [] diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index a9e4402..e43cd33 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -8,6 +8,7 @@ from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict from ..utilities.misc import verify_saved_jacobian +from .support.bijection import Bijection def discont(exogenous, policy, disc_policy, backward, backward_init=None): @@ -168,8 +169,8 @@ def __repr__(self): ''' - def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): + def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, + forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. Parameters diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index bb76e11..fe45715 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -3,6 +3,7 @@ import numpy as np from .support.impulse import ImpulseDict +from .support.bijection import Bijection from ..primitives import Block from .. import utilities as utils from ..steady_state.classes import SteadyStateDict @@ -165,8 +166,8 @@ def __repr__(self): each backward iteration step in td, jacobian is not computed for these! ''' - def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): + def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, + forward_tol=1E-10, forward_maxit=100_000): """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. Parameters @@ -223,7 +224,7 @@ def steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} ss.update(aggregates) - if hetoutput and self.hetoutput is not None: + if self.hetoutput is not None: hetoutputs = self.hetoutput.evaluate(ss) aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") else: diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 25c28cc..412bde1 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -46,7 +46,7 @@ def __init__(self, f): def __repr__(self): return f"" - def steady_state(self, calibration): + def _steady_state(self, calibration): input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ misc.numeric_primitive(self.f(**input_args))] diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 7f37cea..35e0769 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -3,6 +3,7 @@ from ..primitives import Block from ..blocks.simple_block import simple from ..utilities import graph +from .support.bijection import Bijection def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): @@ -39,6 +40,7 @@ class SolvedBlock(Block): def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. self._blocks_unsorted = blocks + self.M = Bijection({}) # don't inherit membrane from parent blocks (think more about this later) # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort @@ -66,8 +68,8 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ def __repr__(self): return f"" - def steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, - consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): + def _steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, + consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index b8bf815..74c5477 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -41,4 +41,13 @@ def __matmul__(self, x): elif isinstance(x, tuple): return tuple(self[k] for k in x) # else: + # would this prevent calling SteadyStateDict.__rmatmul__? # return NotImplementedError + + def __rmatmul__(self, x): + if isinstance(x, dict): + return {self[k]: v for k, v in x.items()} + elif isinstance(x, list): + return [self[k] for k in x] + elif isinstance(x, tuple): + return tuple(self[k] for k in x) \ No newline at end of file diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index c4a13a5..a4309d9 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Union, Tuple, Optional, List from copy import deepcopy -from .steady_state.drivers import steady_state +from .steady_state.drivers import steady_state as ss from .steady_state.support import provide_solver_default from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G @@ -72,11 +72,11 @@ def outputs(self): # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical # input and output argument structure - def steady_state(self, calibration: Dict[str, Union[Real, Array]], - **kwargs) -> SteadyStateDict: - raise NotImplementedError(f'{type(self)} does not implement .steady_state()') - # def steady_state(self, calibration, **kwargs): - # return self.M @ self.steady_state(self.M.inv @ calibration, **kwargs) + # def steady_state(self, calibration: Dict[str, Union[Real, Array]], + # **kwargs) -> SteadyStateDict: + # raise NotImplementedError(f'{type(self)} does not implement .steady_state()') + def steady_state(self, calibration, **kwargs): + return self._steady_state(calibration @ self.M.inv, **kwargs) @ self.M def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: @@ -99,7 +99,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], the target conditions that must hold in general equilibrium""" blocks = self.blocks if hasattr(self, "blocks") else [self] solver = solver if solver else provide_solver_default(unknowns) - return steady_state(blocks, calibration, unknowns, targets, solver=solver, **kwargs) + return ss(blocks, calibration, unknowns, targets, solver=solver, **kwargs) def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index 4f7f154..ee2667c 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -146,10 +146,13 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k """Evaluate the .ss method of a block, given a dictionary of potential arguments""" if toplevel_unknowns is None: toplevel_unknowns = {} - block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False if dissolve is None: dissolve = [] + # Remapping + calibration = calibration @ block.M.inv + toplevel_unknowns = toplevel_unknowns @ block.M.inv + # Add the block's internal variables as inputs, if the block has an internal attribute if isinstance(calibration, dict): input_arg_dict = deepcopy(calibration) @@ -159,6 +162,8 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them # at the provided set of unknowns if included in dissolve. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) + block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr( + block, "unknowns") else False input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} if block in dissolve and "solver" in valid_input_kwargs: input_kwarg_dict["solver"] = "solved" @@ -170,7 +175,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k f"If the user provides a set of top-level unknowns that subsume block-level unknowns," f" it must be explicitly declared in `dissolve`.") - return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) + return block.steady_state(input_arg_dict, **input_kwarg_dict) @ block.M def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, From b6751f441b062b2d9bfe50b79cf13d156cce7134 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 16 Jun 2021 18:52:42 -0500 Subject: [PATCH 172/288] Fixed dissolve. Bijection could not be multiplied with dict_keys. --- src/sequence_jacobian/steady_state/drivers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index ee2667c..b42bf5d 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -145,13 +145,13 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): """Evaluate the .ss method of a block, given a dictionary of potential arguments""" if toplevel_unknowns is None: - toplevel_unknowns = {} + toplevel_unknowns = [] if dissolve is None: dissolve = [] # Remapping calibration = calibration @ block.M.inv - toplevel_unknowns = toplevel_unknowns @ block.M.inv + toplevel_unknowns = list(toplevel_unknowns) @ block.M.inv # Add the block's internal variables as inputs, if the block has an internal attribute if isinstance(calibration, dict): @@ -162,8 +162,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them # at the provided set of unknowns if included in dissolve. valid_input_kwargs = misc.input_kwarg_list(block.steady_state) - block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr( - block, "unknowns") else False + block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} if block in dissolve and "solver" in valid_input_kwargs: input_kwarg_dict["solver"] = "solved" From bdf08e7118d008c379c7158bace4cf7764985c61 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 22 Jun 2021 09:28:24 -0500 Subject: [PATCH 173/288] Test models for remap. (temporary) --- dcblock.py => dcblock_test.py | 2 + remap_test.py | 136 ++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) rename dcblock.py => dcblock_test.py (99%) create mode 100644 remap_test.py diff --git a/dcblock.py b/dcblock_test.py similarity index 99% rename from dcblock.py rename to dcblock_test.py index 5cc87b1..d238c37 100644 --- a/dcblock.py +++ b/dcblock_test.py @@ -1,3 +1,5 @@ +'''This model is to test DisContBlock''' + import numpy as np from numba import guvectorize, njit import dfols diff --git a/remap_test.py b/remap_test.py new file mode 100644 index 0000000..9891a3f --- /dev/null +++ b/remap_test.py @@ -0,0 +1,136 @@ +"""Simple model to test remapping & multiple hetblocks.""" + +import numpy as np +import copy + +from sequence_jacobian import utilities as utils +from sequence_jacobian import create_model, hetoutput, het, simple + + +'''Part 1: HA block''' + + +def household_init(a_grid, e_grid, r, w, eis): + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + return Va + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, Pi_p, a_grid, e_grid, beta, r, w, eis): + """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. + + Parameters + ---------- + Va_p : array (S, A), marginal value of assets tomorrow + Pi_p : array (S, S), Markov matrix for skills tomorrow + a_grid : array (A), asset grid + e_grid : array (S*B), skill grid + beta : scalar, discount rate today + r : scalar, ex-post real interest rate + w : scalar, wage + eis : scalar, elasticity of intertemporal substitution + + Returns + ---------- + Va : array (S*B, A), marginal value of assets today + a : array (S*B, A), asset policy today + c : array (S*B, A), consumption policy today + """ + uc_nextgrid = (beta * Pi_p) @ Va_p + c_nextgrid = uc_nextgrid ** (-eis) + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) + utils.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + Va = (1 + r) * c ** (-1 / eis) + return Va, a, c + + +def get_mpcs(c, a, a_grid, rpost): + """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" + mpcs = np.empty_like(c) + post_return = (1 + rpost) * a_grid + + # symmetric differences away from boundaries + mpcs[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (post_return[2:] - post_return[:-2]) + + # asymmetric first differences at boundaries + mpcs[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) + mpcs[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) + + # special case of constrained + mpcs[a == a_grid[0]] = 1 + + return mpcs + + +'''Part 2: Simple Blocks''' + + +@simple +def firm(K, L, Z, alpha, delta): + r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta + w = (1 - alpha) * Z * (K(-1) / L) ** alpha + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return r, w, Y + + +@simple +def mkt_clearing(K, A, Y, C, delta): + asset_mkt = A - K + goods_mkt = Y - C - delta * K + return asset_mkt, goods_mkt + + +@simple +def income_state_vars(rho, sigma, nS): + e, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + return e, Pi + + +@simple +def asset_state_vars(amax, nA): + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return a_grid + + +@simple +def firm_steady_state_solution(r, delta, alpha): + rk = r + delta + Z = (rk / alpha) ** alpha # normalize so that Y=1 + K = (alpha * Z / rk) ** (1 / (1 - alpha)) + Y = Z * K ** alpha + w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) + return Z, K, Y, w + + +'''Part 3: permanent heterogeneity''' + +# remap method takes a dict and returns new copies of blocks +to_map = ['beta', *household.outputs] +hh_patient = household.remap({k: k + '_patient' for k in to_map}) +hh_impatient = household.remap({k: k + '_impatient' for k in to_map}) + + +@simple +def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): + C = mass_patient * C_patient + (1 - mass_patient) * C_impatient + A = mass_patient * A_patient + (1 - mass_patient) * A_impatient + return C, A + + +'''Steady state''' + +# DAG +blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] +helper_blocks = [firm_steady_state_solution] +ks_model = create_model(blocks, name="Krusell-Smith") + +# # Steady State +# calibration = {"eis": 1, "delta": 0.025, "alpha": 0.3, "rho": 0.966, "sigma": 0.5, "L": 1.0, +# "nS": 11, "nA": 100, "amax": 1000, "r": 0.01, 'beta_impatient': 0.98} +# unknowns_ss = {"beta_patient": (0.98/1.01, 0.999/1.01), "Z": 0.85, "K": 3.} +# targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} +# ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", +# helper_blocks=helper_blocks, helper_targets=["Y", "r"]) From fdb86cbe3f40e9f41a134a1c563a7f6dbc69c3cc Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 22 Jun 2021 14:10:29 -0500 Subject: [PATCH 174/288] Rename method for blocks. --- remap_test.py | 10 +++++----- src/sequence_jacobian/blocks/discont_block.py | 6 +++--- src/sequence_jacobian/blocks/het_block.py | 6 +++--- src/sequence_jacobian/blocks/simple_block.py | 2 +- src/sequence_jacobian/primitives.py | 5 +++++ 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/remap_test.py b/remap_test.py index 9891a3f..ce98027 100644 --- a/remap_test.py +++ b/remap_test.py @@ -109,8 +109,8 @@ def firm_steady_state_solution(r, delta, alpha): # remap method takes a dict and returns new copies of blocks to_map = ['beta', *household.outputs] -hh_patient = household.remap({k: k + '_patient' for k in to_map}) -hh_impatient = household.remap({k: k + '_impatient' for k in to_map}) +hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('patient household') +hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('impatient household') @simple @@ -123,9 +123,9 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): '''Steady state''' # DAG -blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] -helper_blocks = [firm_steady_state_solution] -ks_model = create_model(blocks, name="Krusell-Smith") +# blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] +# helper_blocks = [firm_steady_state_solution] +# ks_model = create_model(blocks, name="Krusell-Smith") # # Steady State # calibration = {"eis": 1, "delta": 0.025, "alpha": 0.3, "rho": 0.966, "sigma": 0.5, "L": 1.0, diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index e43cd33..e31a462 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -150,12 +150,12 @@ def __repr__(self): """Nice string representation of HetBlock for printing to console""" if self.hetinput is not None: if self.hetoutput is not None: - return f"" else: - return f"" + return f"" else: - return f"" + return f"" '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - steady_state : do backward and forward iteration until convergence to get complete steady state diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index fe45715..52312f1 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -148,12 +148,12 @@ def __repr__(self): """Nice string representation of HetBlock for printing to console""" if self.hetinput is not None: if self.hetoutput is not None: - return f"" else: - return f"" + return f"" else: - return f"" + return f"" '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - steady_state : do backward and forward iteration until convergence to get complete steady state diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 412bde1..3ac7f63 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -44,7 +44,7 @@ def __init__(self, f): self.M = Bijection({}) def __repr__(self): - return f"" + return f"" def _steady_state(self, calibration): input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index a4309d9..557ac8b 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -144,3 +144,8 @@ def remap(self, map): remapped = deepcopy(self) remapped.M = self.M @ Bijection(map) return remapped + + def rename(self, name): + renamed = deepcopy(self) + renamed.name = name + return renamed From 2d567e8c82831c4c2e020bb329248c89305aa054 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 22 Jun 2021 14:19:08 -0500 Subject: [PATCH 175/288] Right multiplication of SteadyStateDict with Bijection works. --- src/sequence_jacobian/blocks/support/bijection.py | 5 ++--- src/sequence_jacobian/primitives.py | 5 +---- src/sequence_jacobian/steady_state/drivers.py | 6 +++--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index 74c5477..dabd449 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -40,9 +40,8 @@ def __matmul__(self, x): return [self[k] for k in x] elif isinstance(x, tuple): return tuple(self[k] for k in x) - # else: - # would this prevent calling SteadyStateDict.__rmatmul__? - # return NotImplementedError + else: + return NotImplemented def __rmatmul__(self, x): if isinstance(x, dict): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 557ac8b..82ecf47 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -72,11 +72,8 @@ def outputs(self): # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical # input and output argument structure - # def steady_state(self, calibration: Dict[str, Union[Real, Array]], - # **kwargs) -> SteadyStateDict: - # raise NotImplementedError(f'{type(self)} does not implement .steady_state()') def steady_state(self, calibration, **kwargs): - return self._steady_state(calibration @ self.M.inv, **kwargs) @ self.M + return self.M @ self._steady_state(self.M.inv @ calibration, **kwargs) def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index b42bf5d..bdf34c4 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -150,8 +150,8 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k dissolve = [] # Remapping - calibration = calibration @ block.M.inv - toplevel_unknowns = list(toplevel_unknowns) @ block.M.inv + calibration = block.M.inv @ calibration + toplevel_unknowns = block.M.inv @ list(toplevel_unknowns) # Add the block's internal variables as inputs, if the block has an internal attribute if isinstance(calibration, dict): @@ -174,7 +174,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k f"If the user provides a set of top-level unknowns that subsume block-level unknowns," f" it must be explicitly declared in `dissolve`.") - return block.steady_state(input_arg_dict, **input_kwarg_dict) @ block.M + return block.M @ block.steady_state(input_arg_dict, **input_kwarg_dict) def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, From 2a6ee86bd7e5336b2bc3b5e0c8556b787e0376b6 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 22 Jun 2021 17:48:35 -0500 Subject: [PATCH 176/288] KS model with two types works. Something broke with solve_steady_state. --- remap_test.py | 49 +++++++++++-------- .../blocks/support/bijection.py | 8 ++- src/sequence_jacobian/primitives.py | 12 +++-- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/remap_test.py b/remap_test.py index ce98027..db9c745 100644 --- a/remap_test.py +++ b/remap_test.py @@ -85,8 +85,8 @@ def mkt_clearing(K, A, Y, C, delta): @simple def income_state_vars(rho, sigma, nS): - e, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - return e, Pi + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + return e_grid, Pi @simple @@ -96,41 +96,48 @@ def asset_state_vars(amax, nA): @simple -def firm_steady_state_solution(r, delta, alpha): +def firm_ss(r, Y, L, delta, alpha): rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - return Z, K, Y, w + w = (1 - alpha) * Y / L + K = alpha * Y / rk + Z = Y / K ** alpha / L ** (1 - alpha) + return w, K, Z + + +@hetoutput() +def mpcs(c, a, a_grid, r): + mpc = get_mpcs(c, a, a_grid, r) + return mpc '''Part 3: permanent heterogeneity''' # remap method takes a dict and returns new copies of blocks +household.add_hetoutput(mpcs, verbose=False) to_map = ['beta', *household.outputs] hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('patient household') hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('impatient household') @simple -def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): +def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_impatient, mass_patient): C = mass_patient * C_patient + (1 - mass_patient) * C_impatient A = mass_patient * A_patient + (1 - mass_patient) * A_impatient - return C, A + Mpc = mass_patient * Mpc_patient + (1 - mass_patient) * Mpc_impatient + return C, A, Mpc '''Steady state''' # DAG -# blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] -# helper_blocks = [firm_steady_state_solution] -# ks_model = create_model(blocks, name="Krusell-Smith") - -# # Steady State -# calibration = {"eis": 1, "delta": 0.025, "alpha": 0.3, "rho": 0.966, "sigma": 0.5, "L": 1.0, -# "nS": 11, "nA": 100, "amax": 1000, "r": 0.01, 'beta_impatient': 0.98} -# unknowns_ss = {"beta_patient": (0.98/1.01, 0.999/1.01), "Z": 0.85, "K": 3.} -# targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} -# ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", -# helper_blocks=helper_blocks, helper_targets=["Y", "r"]) +blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] +ks_model = create_model(blocks, name="Krusell-Smith") + +# Steady State +calibration = {'eis': 1, 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, + 'nS': 11, 'nA': 500, 'amax': 1000, 'beta_impatient': 0.98, 'mass_patient': 0.5} +ss = ks_model.solve_steady_state(calibration, solver='brentq', + unknowns={'beta_patient': (0.97/1.01, 0.999/1.01), 'Z': 0.5, 'K': 8.6}, + targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, + helper_blocks=[firm_ss], helper_targets=['Y', 'r']) +# TODO why is this solution so inaccurate? diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index dabd449..834dac8 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -38,6 +38,8 @@ def __matmul__(self, x): return {self[k]: v for k, v in x.items()} elif isinstance(x, list): return [self[k] for k in x] + elif isinstance(x, set): + return {self[k] for k in x} elif isinstance(x, tuple): return tuple(self[k] for k in x) else: @@ -48,5 +50,9 @@ def __rmatmul__(self, x): return {self[k]: v for k, v in x.items()} elif isinstance(x, list): return [self[k] for k in x] + elif isinstance(x, set): + return {self[k] for k in x} elif isinstance(x, tuple): - return tuple(self[k] for k in x) \ No newline at end of file + return tuple(self[k] for k in x) + else: + return NotImplemented \ No newline at end of file diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 82ecf47..304fad0 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -138,9 +138,15 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) def remap(self, map): - remapped = deepcopy(self) - remapped.M = self.M @ Bijection(map) - return remapped + other = deepcopy(self) + other.M = self.M @ Bijection(map) + other.inputs = other.M @ self.inputs + other.outputs = other.M @ self.outputs + if hasattr(self, 'input_list'): + other.input_list = other.M @ self.input_list + if hasattr(self, 'output_list'): + other.output_list = other.M @ self.output_list + return other def rename(self, name): renamed = deepcopy(self) From e7ae8c757cb008024e791c3909c1207dcc1fcc50 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 22 Jun 2021 18:13:07 -0500 Subject: [PATCH 177/288] Fixed steady_state driver. --- remap_test.py | 1 - src/sequence_jacobian/steady_state/drivers.py | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/remap_test.py b/remap_test.py index db9c745..3811bae 100644 --- a/remap_test.py +++ b/remap_test.py @@ -140,4 +140,3 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i unknowns={'beta_patient': (0.97/1.01, 0.999/1.01), 'Z': 0.5, 'K': 8.6}, targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, helper_blocks=[firm_ss], helper_targets=['Y', 'r']) -# TODO why is this solution so inaccurate? diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index bdf34c4..3119bfa 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -149,10 +149,6 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k if dissolve is None: dissolve = [] - # Remapping - calibration = block.M.inv @ calibration - toplevel_unknowns = block.M.inv @ list(toplevel_unknowns) - # Add the block's internal variables as inputs, if the block has an internal attribute if isinstance(calibration, dict): input_arg_dict = deepcopy(calibration) @@ -174,7 +170,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k f"If the user provides a set of top-level unknowns that subsume block-level unknowns," f" it must be explicitly declared in `dissolve`.") - return block.M @ block.steady_state(input_arg_dict, **input_kwarg_dict) + return block.M @ block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, From eab87604fb4382561bf2b0ce3e495343b018c6a0 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 23 Jun 2021 09:22:31 -0500 Subject: [PATCH 178/288] Remapping JacobianDicts and ImpulseDicts in progress. --- remap_test.py | 3 +++ .../blocks/combined_block.py | 6 ++--- src/sequence_jacobian/blocks/discont_block.py | 6 ++--- src/sequence_jacobian/blocks/het_block.py | 6 ++--- src/sequence_jacobian/blocks/simple_block.py | 6 ++--- src/sequence_jacobian/blocks/solved_block.py | 6 ++--- .../blocks/support/impulse.py | 12 ++++++++++ src/sequence_jacobian/jacobian/classes.py | 1 + src/sequence_jacobian/primitives.py | 23 ++++++++++--------- 9 files changed, 43 insertions(+), 26 deletions(-) diff --git a/remap_test.py b/remap_test.py index 3811bae..14a88a4 100644 --- a/remap_test.py +++ b/remap_test.py @@ -140,3 +140,6 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i unknowns={'beta_patient': (0.97/1.01, 0.999/1.01), 'Z': 0.5, 'K': 8.6}, targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, helper_blocks=[firm_ss], helper_targets=['Y', 'r']) + +td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, + unknowns=['K'], targets=['asset_mkt']) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 1cbf863..b28108c 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -71,7 +71,7 @@ def _steady_state(self, calibration, helper_blocks=None, **kwargs): ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) return SteadyStateDict(ss_partial_eq) - def impulse_nonlinear(self, ss, exogenous, **kwargs): + def _impulse_nonlinear(self, ss, exogenous, **kwargs): """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state, `ss`""" irf_nonlin_partial_eq = deepcopy(exogenous) @@ -83,7 +83,7 @@ def impulse_nonlinear(self, ss, exogenous, **kwargs): return ImpulseDict(irf_nonlin_partial_eq) - def impulse_linear(self, ss, exogenous, T=None, Js=None): + def _impulse_linear(self, ss, exogenous, T=None, Js=None): """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from a steady_state, `ss`""" irf_lin_partial_eq = deepcopy(exogenous) @@ -95,7 +95,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(irf_lin_partial_eq) - def jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): + def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at a steady state, `ss`""" if exogenous is None: diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index e31a462..091acda 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -235,7 +235,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict(ss, internal=self) - def impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=None): + def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for DiscontBlock given dynamic paths for inputs in exogenous, assuming that we start and end in steady state ss, and that all inputs not specified in exogenous are constant at their ss values. Analog to SimpleBlock.td. @@ -367,7 +367,7 @@ def impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=No else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): + def _impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): # infer T from exogenous, check that all shocks have same length shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: @@ -376,7 +376,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None, h=1E-4): + def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 52312f1..2af7d55 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -234,7 +234,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict(ss, internal=self) - def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): + def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, assuming that we start and end in steady state ss, and that all inputs not specified in kwargs are constant at their ss values. Analog to SimpleBlock.td. @@ -345,7 +345,7 @@ def impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fal else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): + def _impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): # infer T from exogenous, check that all shocks have same length shock_lengths = [x.shape[0] for x in exogenous.values()] if shock_lengths[1:] != shock_lengths[:-1]: @@ -354,7 +354,7 @@ def impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): + def _jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 3ac7f63..ae6ee2b 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -52,7 +52,7 @@ def _steady_state(self, calibration): misc.numeric_primitive(self.f(**input_args))] return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) - def impulse_nonlinear(self, ss, exogenous): + def _impulse_nonlinear(self, ss, exogenous): input_args = {} for k, v in exogenous.items(): if np.isscalar(v): @@ -65,10 +65,10 @@ def impulse_nonlinear(self, ss, exogenous): return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) - ss - def impulse_linear(self, ss, exogenous, T=None, Js=None): + def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) - def jacobian(self, ss, exogenous=None, T=None, Js=None): + def _jacobian(self, ss, exogenous=None, T=None, Js=None): """Assemble nested dict of Jacobians Parameters diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 35e0769..efaa614 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -87,18 +87,18 @@ def _steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=N return super().solve_steady_state(calibration, unknowns, self.targets, solver=solver, consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) - def impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): + def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): return super().solve_impulse_nonlinear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), Js=Js, targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - def impulse_linear(self, ss, exogenous, T=None, Js=None): + def _impulse_linear(self, ss, exogenous, T=None, Js=None): return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), T=T, Js=Js) - def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): + def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): if exogenous is None: exogenous = list(self.inputs) if outputs is None: diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index 4be8f93..c8fe87f 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -1,8 +1,10 @@ """ImpulseDict class for manipulating impulse responses.""" import numpy as np +from copy import deepcopy from ...steady_state.classes import SteadyStateDict +from .bijection import Bijection class ImpulseDict: @@ -67,3 +69,13 @@ def __truediv__(self, other): # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero if isinstance(other, SteadyStateDict): return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) + + def __matmul__(self, x): + # remap keys in toplevel + if isinstance(x, Bijection): + new = deepcopy(self) + new.impulse = x @ self.impulse + return new + + def __rmatmul__(self, x): + return self.__matmul__(x) diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index b522f7d..572c23e 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -5,6 +5,7 @@ import numpy as np from . import support +from ..blocks.support.bijection import Bijection class Jacobian(metaclass=ABCMeta): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 304fad0..06fae56 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -70,22 +70,23 @@ def inputs(self): def outputs(self): pass - # Typing information is purely to inform future user-developed `Block` sub-classes to enforce a canonical - # input and output argument structure def steady_state(self, calibration, **kwargs): + """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" return self.M @ self._steady_state(self.M.inv @ calibration, **kwargs) - def impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: - raise NotImplementedError(f'{type(self)} does not implement .impulse_nonlinear()') + def impulse_nonlinear(self, ss, exogenous, **kwargs): + """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks + from a steady state `ss`.""" + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: - raise NotImplementedError(f'{type(self)} does not implement .impulse_linear()') + def impulse_linear(self, ss, exogenous, **kwargs): + """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks + from a steady state `ss`.""" + return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def jacobian(self, ss: Dict[str, Union[Real, Array]], exogenous: List[str] = None, - T: int = None, **kwargs) -> JacobianDict: - raise NotImplementedError(f'{type(self)} does not implement .jacobian()') + def jacobian(self, ss, exogenous, **kwargs): + """Calculate a partial equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`.""" + return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], From 9363bfd28b031fde0eeea92a3441e0a10b61f6e2 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 23 Jun 2021 13:03:25 -0500 Subject: [PATCH 179/288] Remap is done! --- remap_test.py | 7 ++++--- src/sequence_jacobian/blocks/combined_block.py | 2 +- src/sequence_jacobian/jacobian/classes.py | 9 +++++++++ src/sequence_jacobian/primitives.py | 4 ++-- tests/base/test_jacobian.py | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/remap_test.py b/remap_test.py index 14a88a4..0419171 100644 --- a/remap_test.py +++ b/remap_test.py @@ -73,13 +73,14 @@ def firm(K, L, Z, alpha, delta): r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta w = (1 - alpha) * Z * (K(-1) / L) ** alpha Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return r, w, Y + I = K - (1 - delta) * K(-1) + return r, w, Y, I @simple -def mkt_clearing(K, A, Y, C, delta): +def mkt_clearing(K, A, Y, C, I): asset_mkt = A - K - goods_mkt = Y - C - delta * K + goods_mkt = Y - C - I return asset_mkt, goods_mkt diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index b28108c..3c355c9 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -105,7 +105,7 @@ def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "Js": Js} for i, block in enumerate(self.blocks): - curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block.jacobian) if k in kwargs}).complete() + curlyJ = block._jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block._jacobian) if k in kwargs}).complete() # If we want specific list of outputs, restrict curlyJ to that before continuing curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index 572c23e..d5e1cdc 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -386,9 +386,18 @@ def addinputs(self): def __matmul__(self, x): if isinstance(x, JacobianDict): return self.compose(x) + elif isinstance(x, Bijection): + nesteddict = x @ self.nesteddict + for o in nesteddict.keys(): + nesteddict[o] = x @ nesteddict[o] + return JacobianDict(nesteddict, inputs=x @ self.inputs, outputs=x @ self.outputs) else: return self.apply(x) + def __rmatmul__(self, x): + if isinstance(x, Bijection): + return JacobianDict(x @ self.nesteddict, inputs=x @ self.inputs, outputs=x @ self.outputs) + def __bool__(self): return bool(self.outputs) and bool(self.inputs) diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 06fae56..a8ed6a9 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -84,9 +84,9 @@ def impulse_linear(self, ss, exogenous, **kwargs): from a steady state `ss`.""" return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def jacobian(self, ss, exogenous, **kwargs): + def jacobian(self, ss, exogenous, T=None, **kwargs): """Calculate a partial equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`.""" - return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) + return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, T=T, **kwargs) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 431b36d..6bd4a1d 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -126,7 +126,7 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): household = hank_model._blocks_unsorted[0] T = 40 - exogenous = 'r' + exogenous = ['r'] output_list = household.non_back_iter_outputs h = 1E-4 From 440fc836ad2b87e2d82ef02b244aca805604cb77 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 23 Jun 2021 13:29:55 -0500 Subject: [PATCH 180/288] Simple models to test dcblock and remap. --- dcblock_test.py => tests/dcblock_test.py | 2 +- remap_test.py => tests/remap_test.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename dcblock_test.py => tests/dcblock_test.py (99%) rename remap_test.py => tests/remap_test.py (100%) diff --git a/dcblock_test.py b/tests/dcblock_test.py similarity index 99% rename from dcblock_test.py rename to tests/dcblock_test.py index d238c37..9540471 100644 --- a/dcblock_test.py +++ b/tests/dcblock_test.py @@ -1,4 +1,4 @@ -'''This model is to test DisContBlock''' +"""This model is to test DisContBlock""" import numpy as np from numba import guvectorize, njit diff --git a/remap_test.py b/tests/remap_test.py similarity index 100% rename from remap_test.py rename to tests/remap_test.py From c4cdb434a7cd05bc351fd1df93df083f64b1434b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 23 Jun 2021 13:37:17 -0500 Subject: [PATCH 181/288] Signature with types for main block methods in primitives.py. --- src/sequence_jacobian/primitives.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index a8ed6a9..c8201ad 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -70,21 +70,25 @@ def inputs(self): def outputs(self): pass - def steady_state(self, calibration, **kwargs): + def steady_state(self, calibration: SteadyStateDict, **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" return self.M @ self._steady_state(self.M.inv @ calibration, **kwargs) - def impulse_nonlinear(self, ss, exogenous, **kwargs): + def impulse_nonlinear(self, ss: SteadyStateDict, + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`.""" return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def impulse_linear(self, ss, exogenous, **kwargs): + def impulse_linear(self, ss: SteadyStateDict, + exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from a steady state `ss`.""" return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def jacobian(self, ss, exogenous, T=None, **kwargs): + def jacobian(self, ss: SteadyStateDict, + exogenous: Dict[str, Array], + T: Optional[int] = None, **kwargs) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`.""" return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, T=T, **kwargs) From bbf89691fc67b6ae68bcf078ab1e58ab92a602c9 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 24 Jun 2021 14:18:46 -0500 Subject: [PATCH 182/288] Model w 2 remapped CombinedBlocks works well, but should return internals. --- tests/dcblock_test.py | 68 +++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/tests/dcblock_test.py b/tests/dcblock_test.py index 9540471..40cf638 100644 --- a/tests/dcblock_test.py +++ b/tests/dcblock_test.py @@ -116,9 +116,8 @@ def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, z_grid, b_g a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) # e. upper envelope - # V, c = upper_envelope(W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region - V, c = upper_envelope_fast(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) + V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) # f. update Va uc = c ** (-1 / eis) @@ -208,7 +207,7 @@ def nonconcave(uc_nextgrid, imin, imax): @njit -def upper_envelope_fast(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): +def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): """ Interpolate consumption and value function to exogenous grid. Brute force but safe. Parameters @@ -341,7 +340,6 @@ def firm(Z, K, L, mc, alpha, delta0, delta1, psi): Y = Z * K(-1) ** alpha * L ** (1 - alpha) w = (1 - alpha) * mc * Y / L u = (alpha / delta0 / delta1 * mc * Y / K(-1)) ** (1 / delta1) - # u = 1.0 delta = delta0 * u ** delta1 Q = 1 + psi * (K / K(-1) - 1) I = K - (1 - delta) * K(-1) + psi / 2 * (K / K(-1) - 1) ** 2 * K(-1) @@ -399,9 +397,25 @@ def mkt_clearing(A, B, Y, C, I, G, L, Ze): return asset_mkt, goods_mkt, labor_mkt +@simple +def dividends(transfer): + transfer_sm = transfer + transfer_sw = transfer + return transfer_sm, transfer_sw + + +@simple +def aggregate(A_sm, C_sm, Ze_sm, Ui_sm, A_sw, C_sw, Ze_sw, Ui_sw): + A = (A_sm + A_sw) / 2 + C = (C_sm + C_sw) / 2 + Ze = (Ze_sm + Ze_sw) / 2 + Ui = (Ui_sm + Ui_sw) / 2 + return A, C, Ze, Ui + + @simple def helper1(A, tax, w, Ze, rpost, Ui): - # after hh + # after hh block B = A G = tax * w * Ze - rpost * B - (1 - tax) * w * Ui return B, G @@ -420,24 +434,42 @@ def helper2(eps, rpost, Y, L, alpha, delta0): '''Try this''' +cali_sm = {'beta': 0.9782, 'vphi': 0.8079, 'chi': 0.5690, 'fU': 0.25, 'fN': 0.1174, 's': 0.0218, + 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, + 'fU_eps': 10.69, 'fN_eps': 5.57, 's_eps': -11.17} + +cali_sw = {'beta': 0.9830, 'vphi': 0.9909, 'chi': 0.4982, 'fU': 0.22, 'fN': 0.1099, 's': 0.0132, + 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, + 'fU_eps': 8.72, 'fN_eps': 3.55, 's_eps': -6.55} + +cali_sm = {k + '_sm': v for k, v in cali_sm.items()} +cali_sw = {k + '_sw': v for k, v in cali_sw.items()} + +calibration = {**cali_sm, **cali_sw, + 'eis': 1.0, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'eps': 10.0, 'tax': 0.3, + 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, 'nZ': 7, + 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0.0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083} + +hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows, household], name='Single') + + +to_map = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', 'fU_eps', 'fN_eps', 's_eps', + *hh.outputs] +hh_sm = hh.remap({k: k + '_sm' for k in to_map}).rename('SingleMen') +hh_sw = hh.remap({k: k + '_sw' for k in to_map}).rename('SingleWomen') -calibration = {'eis': 1.0, 'vphi': 0.65, 'chi': 0.6, - 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'fU': 0.25, 'fN': 0.1, 's': 0.025, - 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, - 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'nZ': 7, 'beta': 0.985, - 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083, - 'eps': 10, 'fU_eps': 10.0, 'fN_eps': 5.0, 's_eps': -8.0, 'tax': 0.3} -hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, household], name='SingleMen') -hank = create_model([hh, firm, monetary, valuation, nkpc, fiscal1, fiscal2, flows, mkt_clearing], name='HANK') +hank = create_model([hh_sm, hh_sw, aggregate, dividends, firm, monetary, valuation, nkpc, fiscal1, fiscal2, + mkt_clearing], name='HANK') -ss = hank.solve_steady_state(calibration, solver='hybr', dissolve=[fiscal1, flows], - unknowns={'Z': 0.63, 'L': 0.86, 'mc': 0.9, 'G': 0.2, 'delta1': 1.2, - 'B': 3.0, 'K': 17.0}, - targets={'Y': 1.0, 'labor_mkt': 0.0, 'nkpc_res': 0.0, 'budget': 0.0, - 'asset_mkt': 0.0, 'val': 0.0, 'u': 1.0}, +ss = hank.solve_steady_state(calibration, solver='brentq', dissolve=[fiscal1, flows], + unknowns={'L': (0.7, 0.72), + 'Z': 0.63, 'mc': 0.9, 'G': 0.2, 'delta1': 1.2, 'B': 3.0, 'K': 17.0}, + targets={'labor_mkt': 0.0, + 'Y': 1.0, 'nkpc_res': 0.0, 'budget': 0.0, 'asset_mkt': 0.0, 'val': 0.0, 'u': 1.0}, helper_blocks=[helper1, helper2], helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) +out = hh_sm.steady_state(ss) From f649f923711212551539b3dadda7ef99bbf95a05 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 24 Jun 2021 16:50:48 -0500 Subject: [PATCH 183/288] Calibrated ss of full model w couples and singles. --- tests/couple_endo.py | 404 +++++++++++++++++++++++++++++++++++++ tests/dcblock_test.py | 450 +++++++----------------------------------- tests/single_endo.py | 341 ++++++++++++++++++++++++++++++++ 3 files changed, 811 insertions(+), 384 deletions(-) create mode 100644 tests/couple_endo.py create mode 100644 tests/single_endo.py diff --git a/tests/couple_endo.py b/tests/couple_endo.py new file mode 100644 index 0000000..b70df73 --- /dev/null +++ b/tests/couple_endo.py @@ -0,0 +1,404 @@ +"""Couple household model with frictional labor supply.""" + +import numpy as np +from numba import guvectorize, njit + +from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved +from sequence_jacobian.blocks.discont_block import discont +from sequence_jacobian.utilities.misc import choice_prob, logsum + + +'''Core HA block''' + + +def household_init(a_grid, z_grid_m, b_grid_m, z_grid_f, b_grid_f, + atw, transfer, rpost, eis, vphi_m, chi_m, vphi_f, chi_f): + _, income = labor_income(z_grid_m, b_grid_m, z_grid_f, b_grid_f, atw, transfer, 1, 1, 0, 0, 0, 0, 0, 0) + coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + c_guess = 0.3 * coh + V, Va = np.empty_like(coh), np.empty_like(coh) + for iw in range(16): + V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi_m, chi_m, vphi_f, chi_f) + V = V / 0.1 + + # get Va by finite difference + Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) + Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) + Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) + + return Va, V + + +@njit(fastmath=True) +def util(c, iw, eis, vphi_m, chi_m, vphi_f, chi_f): + """Utility function.""" + # 1. utility from consumption. + if eis == 1: + u = np.log(c / 2) + else: + u = (c/2) ** (1 - 1/eis) / (1 - 1/eis) + + # 2. disutility from work and search + if iw <= 3: + u = u - vphi_m # E male + if (iw >= 4) & (iw <= 11): + u = u - chi_m # U male + if np.mod(iw, 4) == 0: + u = u - vphi_f # E female + if (np.mod(iw, 4) == 1) | (np.mod(iw, 4) == 2): + u = u - chi_f # U female + return u + + +@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) +def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, lam_grid, + z_all, b_all, z_man, b_man, z_wom, b_wom, eis, beta, rpost, vphi_m, chi_m, vphi_f, chi_f): + """ + Backward step function EGM with upper envelope. + + Dimensions: 0: labor market status, 1: productivity, 2: assets. + Status: 0: EE, 1: EUb, 2: EU, 3: EN, 4: UbE, 5: UbUb, 6: UbU, 7: UbN, 8: UE, 9: UUb, 10: UU, 11: UN, + 12: NE, 13: NUb, 14: NU, 15: NN + State: 0: MM, 1: MB, 2: ML, 3: BM, 4: BB, 5: BL, 6: LM, 7: LB, 8: LL + + Parameters + ---------- + V_p : array(Ns, Nz, Na), status-specific value function tomorrow + Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow + Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks + Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity + choice_set : list(Nz), discrete choices available in each state X + a_grid : array(Na), exogenous asset grid + y_grid : array(Ns, Nz), exogenous labor income grid + lam_grid : array(Nx), scale of taste shocks, specific to interim state + eis : float, EIS + beta : float, discount factor + rpost : float, ex-post interest rate + + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function today + Va : array(Ns, Nz, Na), partial of status-specific value function today + P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x + c : array(Ns, Nz, Na), status-specific consumption policy today + a : array(Ns, Nz, Na), status-specific asset policy today + ze : array(Ns, Nz, Na), effective labor (average productivity if employed) + ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) + """ + # shapes + Ns, Nz, Na = V_p.shape + Nx = Pi_s_p.shape[1] + + # PART 1: update value and policy functions + # a. discrete choice I expect to make tomorrow + V_p_X = np.empty((Nx, Nz, Na)) + Va_p_X = np.empty((Nx, Nz, Na)) + for ix in range(Nx): + V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) + Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) + P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) + V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) + Va_p_X[ix, ...] = np.sum(P_p_ix*Va_p_ix, axis=0) + + # b. compute expectation wrt labor market shock + V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) + Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) + + # b. compute expectation wrt productivity + V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) + Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) + + # d. consumption today on tomorrow's grid and endogenous asset grid today + W = beta * V_p2 + uc_nextgrid = beta * Va_p2 + c_nextgrid = 2 ** (1 - eis) * uc_nextgrid ** (-eis) + a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) + + # e. upper envelope + imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region + V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi_m, chi_m, + vphi_f, chi_f) + + # f. update Va + uc = 2 ** (1 / eis - 1) * c ** (-1 / eis) + Va = (1 + rpost) * uc + + # PART 2: things we need for GE + # 2/a. asset policy + a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c + + # 2/b. choice probabilities (don't need jacobian) + P = np.zeros((Nx, Ns, Nz, Na)) + for ix in range(Nx): + V_ix = np.take(V, indices=choice_set[ix], axis=0) + P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) + + # 2/c. average productivity of employed + ze = np.zeros_like(a) + ze[0, ...] = z_all[:, np.newaxis] + ze[1, ...], ze[2, ...], ze[3, ...] = z_man[:, np.newaxis], z_man[:, np.newaxis], z_man[:, np.newaxis] + ze[4, ...], ze[8, ...], ze[12, ...] = z_wom[:, np.newaxis], z_wom[:, np.newaxis], z_wom[:, np.newaxis] + + # 2/d. UI claims + ui = np.zeros_like(a) + ui[5, ...] = b_all[:, np.newaxis] + ui[4, ...], ui[6, ...], ui[7, ...] = b_man[:, np.newaxis], b_man[:, np.newaxis], b_man[:, np.newaxis] + ui[1, ...], ui[9, ...], ui[13, ...] = b_wom[:, np.newaxis], b_wom[:, np.newaxis], b_wom[:, np.newaxis] + + return V, Va, a, c, P, ze, ui + + +def labor_income(z_grid_m, b_grid_m, z_grid_f, b_grid_f, atw, transfer, + fU_m, fN_m, s_m, fU_f, fN_f, s_f, rho, expiry): + # 1. income + yE_m, yE_f = atw * z_grid_m + transfer, atw * z_grid_f + yU_m, yU_f = atw * b_grid_m + transfer, atw * b_grid_f + yN_m, yN_f = np.zeros_like(yE_m) + transfer, np.zeros_like(yE_f) + y_EE = (yE_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() + y_EU = (yE_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() + y_EN = (yE_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() + y_UE = (yU_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() + y_UU = (yU_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() + y_UN = (yU_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() + y_NE = (yN_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() + y_NU = (yN_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() + y_NN = (yN_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() + y_grid = np.vstack((y_EE, y_EU, y_EN, y_EN, y_UE, y_UU, y_UN, y_UN, y_NE, y_NU, y_NN, y_NN, y_NE, y_NU, y_NN, y_NN)) + + # 2. transition matrix for joint labor market status + cov = rho * np.sqrt(s_m * s_f * (1 - s_m) * (1 - s_f)) + Pi_s_m = np.array([[1 - s_m, s_m, 0], [fU_m, (1 - fU_m) * (1 - expiry), (1 - fU_m) * expiry], + [fU_m, 0, 1 - fU_m], [fN_m, 0, 1 - fN_m]]) + Pi_s_f = np.array([[1 - s_f, s_f, 0], [fU_f, (1 - fU_f) * (1 - expiry), (1 - fU_f) * expiry], + [fU_f, 0, 1 - fU_f], [fN_f, 0, 1 - fN_f]]) + Pi_s = np.kron(Pi_s_m, Pi_s_f) + + # adjust for correlated job loss + Pi_s[0, 0] += cov # neither loses their job + Pi_s[0, 4] += cov # both lose their job + Pi_s[0, 1] -= cov # only female loses her job + Pi_s[0, 3] -= cov # only male loses his job + + return Pi_s, y_grid + + +"""Supporting functions for HA block""" + + +@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) +def nonconcave(uc_nextgrid, imin, imax): + """Obtain bounds for non-concave region.""" + nA = uc_nextgrid.shape[-1] + vmin = np.inf + vmax = -np.inf + # step 1: find vmin & vmax + for ia in range(nA - 1): + if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: + vmin_temp = uc_nextgrid[ia] + vmax_temp = uc_nextgrid[ia + 1] + if vmin_temp < vmin: + vmin = vmin_temp + if vmax_temp > vmax: + vmax = vmax_temp + + # 2/a Find imin (upper bound) + if vmin == np.inf: + imin_ = 0 + else: + ia = 0 + while ia < nA: + if uc_nextgrid[ia] < vmin: + break + ia += 1 + imin_ = ia + + # 2/b Find imax (lower bound) + if vmax == -np.inf: + imax_ = nA + else: + ia = nA + while ia > 0: + if uc_nextgrid[ia] > vmax: + break + ia -= 1 + imax_ = ia + + imin[:] = imin_ + imax[:] = imax_ + + +@njit +def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): + """ + Interpolate consumption and value function to exogenous grid. Brute force but safe. + Parameters + ---------- + W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) + a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) + c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) + a_grid : array(Na), exogenous asset grid (tomorrow's grid) + y_grid : array(Ns, Nz), labor income + rpost : float, ex-post interest rate + args : (eis, vphi, chi) arguments for utility function + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function on exogenous grid + c : array(Ns, Nz, Na), consumption on exogenous grid + """ + + # 0. initialize + Ns, Nz, Na = W.shape + c = np.zeros_like(W) + V = -np.inf * np.ones_like(W) + + # outer loop could run in parallel + for iw in range(Ns): + for iz in range(Nz): + ycur = y_grid[iw, iz] + imaxcur = imax[iw, iz] + imincur = imin[iw, iz] + + # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. + # in concave region: exploit monotonicity and don't look for extra solutions + for ia in range(Na): + acur = a_grid[ia] + + # Region 1: below non-concave: exploit monotonicity + if (ia <= imaxcur) | (ia >= imincur): + iap = 0 + ap_low = a_nextgrid[iw, iz, iap] + ap_high = a_nextgrid[iw, iz, iap + 1] + while iap < Na - 2: # can this go up all the way? + if ap_high >= acur: + break + iap += 1 + ap_low = ap_high + ap_high = a_nextgrid[iw, iz, iap + 1] + # found bracket, interpolate value function and consumption + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess + c[iw, iz, ia] = c_guess + + # Region 2: non-concave region + else: + # try out all segments of endogenous grid + for iap in range(Na - 1): + # does this endogenous segment bracket ia? + ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] + interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) + + # does it need to be extrapolated above the endogenous grid? + # if needed to be extrapolated below, we would be in constrained case + extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) + + if interp or extrap_above: + # interpolation slopes + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + + # implied guess + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + + # value + v_guess = util(c_guess, iw, *args) + w_guess + + # select best value for this segment + if v_guess > V[iw, iz, ia]: + V[iw, iz, ia] = v_guess + c[iw, iz, ia] = c_guess + + # 2. constrained case: remember that we have the inverse asset policy a(a') + ia = 0 + while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: + c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur + V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] + ia += 1 + + return V, c + + +'''Simple blocks''' + + +def zgrids_couple(zm, zf): + """Combine individual z_grid and b_grid as needed for couple level.""" + zeroes = np.zeros_like(zm) + + # all combinations + z_all = (zm[:, np.newaxis] + zf[np.newaxis, :]).ravel() + z_men = (zm[:, np.newaxis] + zeroes[np.newaxis, :]).ravel() + z_wom = (zeroes[:, np.newaxis] + zf[np.newaxis, :]).ravel() + return z_all, z_men, z_wom + + +@simple +def income_state_vars(mean_m, rho_m, sd_m, mean_f, rho_f, sd_f, nZ, uirate, uicap): + # income husband + z_grid_m, z_pdf_m, z_markov_m = markov_rouwenhorst(rho=rho_m, sigma=sd_m, N=nZ) + z_grid_m *= mean_m + b_grid_m = uirate * z_grid_m + b_grid_m[b_grid_m > uicap] = uicap + + # income wife + z_grid_f, z_pdf_f, z_markov_f = markov_rouwenhorst(rho=rho_f, sigma=sd_f, N=nZ) + z_grid_f *= mean_f + b_grid_f = uirate * z_grid_f + b_grid_f[b_grid_f > uicap] = uicap + + # household income + z_all, z_man, z_wom = zgrids_couple(z_grid_m, z_grid_f) + b_all, b_man, b_wom = zgrids_couple(b_grid_m, b_grid_f) + Pi_z = np.kron(z_markov_m, z_markov_f) + + return z_grid_m, z_grid_f, b_grid_m, b_grid_f, z_all, z_man, z_wom, b_all, b_man, b_wom, Pi_z + + +@simple +def employment_state_vars(lamM, lamB, lamL): + choice_set = [[0, 3, 12, 15], [1, 3, 13, 15], [2, 3, 14, 15], [4, 7, 12, 15], [5, 7, 13, 15], + [6, 7, 14, 15], [8, 11, 12, 15], [9, 11, 13, 15], [10, 11, 14, 15]] + lam_grid = np.array([np.sqrt(2*lamM**2), np.sqrt(lamM**2 + lamB**2), np.sqrt(lamM**2 + lamL**2), + np.sqrt(lamB**2 + lamM**2), np.sqrt(2*lamB**2), np.sqrt(lamB**2 + lamL**2), + np.sqrt(lamL**2 + lamM**2), np.sqrt(lamL**2 + lamB**2), np.sqrt(2*lamL**2)]) + return choice_set, lam_grid + + +@simple +def asset_state_vars(amin, amax, nA): + a_grid = agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +@solved(unknowns={'fU_m': 0.25, 'fN_m': 0.1, 's_m': 0.025}, + targets=['fU_m_res', 'fN_m_res', 's_m_res'], + solver='broyden_custom') +def flows_m(Y, fU_m, fN_m, s_m, fU_eps_m, fN_eps_m, s_eps_m): + fU_m_res = fU_m.ss * (Y / Y.ss) ** fU_eps_m - fU_m + fN_m_res = fN_m.ss * (Y / Y.ss) ** fN_eps_m - fN_m + s_m_res = s_m.ss * (Y / Y.ss) ** s_eps_m - s_m + return fU_m_res, fN_m_res, s_m_res + + +@solved(unknowns={'fU_f': 0.25, 'fN_f': 0.1, 's_f': 0.025}, + targets=['fU_f_res', 'fN_f_res', 's_f_res'], + solver='broyden_custom') +def flows_f(Y, fU_f, fN_f, s_f, fU_eps_f, fN_eps_f, s_eps_f): + fU_f_res = fU_f.ss * (Y / Y.ss) ** fU_eps_f - fU_f + fN_f_res = fN_f.ss * (Y / Y.ss) ** fN_eps_f - fN_f + s_f_res = s_f.ss * (Y / Y.ss) ** s_eps_f - s_f + return fU_f_res, fN_f_res, s_f_res + + +'''Put it together''' + +household.add_hetinput(labor_income, verbose=False) +hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows_m, flows_f, household], + name='CoupleHH') diff --git a/tests/dcblock_test.py b/tests/dcblock_test.py index 40cf638..4e59229 100644 --- a/tests/dcblock_test.py +++ b/tests/dcblock_test.py @@ -1,342 +1,17 @@ """This model is to test DisContBlock""" import numpy as np -from numba import guvectorize, njit -import dfols - -from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved -from sequence_jacobian.blocks.discont_block import discont -from sequence_jacobian.utilities.misc import choice_prob, logsum -from sequence_jacobian.steady_state.classes import SteadyStateDict - - -'''Core HA block''' - - -def household_init(a_grid, z_grid, b_grid, atw, transfer, rpost, eis, vphi, chi): - _, income = labor_income(z_grid, b_grid, atw, transfer, 0, 1, 1, 1) - coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] - c_guess = 0.3 * coh - V, Va = np.empty_like(coh), np.empty_like(coh) - for iw in range(4): - V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi, chi) - V = V / 0.1 - - # get Va by finite difference - Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) - Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) - Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) - - return Va, V - - -@njit(fastmath=True) -def util(c, iw, eis, vphi, chi): - """Utility function.""" - # 1. utility from consumption. - if eis == 1: - u = np.log(c) - else: - u = c ** (1 - 1 / eis) / (1 - 1 / eis) - - # 2. disutility from work and search - if iw == 0: - u = u - vphi # E - elif (iw == 1) | (iw == 2): - u = u - chi # Ub, U - - return u - - -@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) -def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, z_grid, b_grid, lam_grid, eis, beta, rpost, - vphi, chi): - """ - Backward step function EGM with upper envelope. - - Dimensions: 0: labor market status, 1: productivity, 2: assets. - Status: 0: E, 1: Ub, 2: U, 3: O - State: 0: M, 1: B, 2: L - - Parameters - ---------- - V_p : array(Ns, Nz, Na), status-specific value function tomorrow - Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow - Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks - Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity - choice_set : list(Nz), discrete choices available in each state X - a_grid : array(Na), exogenous asset grid - y_grid : array(Ns, Nz), exogenous labor income grid - z_grid : array(Nz), productivity of employed (need for GE) - b_grid : array(Nz), productivity of unemployed (need for GE) - lam_grid : array(Nx), scale of taste shocks, specific to interim state - eis : float, EIS - beta : float, discount factor - rpost : float, ex-post interest rate - vphi : float, disutility of work - chi : float, disutility of search - - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function today - Va : array(Ns, Nz, Na), partial of status-specific value function today - P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x - c : array(Ns, Nz, Na), status-specific consumption policy today - a : array(Ns, Nz, Na), status-specific asset policy today - ze : array(Ns, Nz, Na), effective labor (average productivity if employed) - ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) - """ - # shapes - Ns, Nz, Na = V_p.shape - Nx = Pi_s_p.shape[1] - - # PART 1: update value and policy functions - # a. discrete choice I expect to make tomorrow - V_p_X = np.empty((Nx, Nz, Na)) - Va_p_X = np.empty((Nx, Nz, Na)) - for ix in range(Nx): - V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) - Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) - P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) - V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) - Va_p_X[ix, ...] = np.sum(P_p_ix * Va_p_ix, axis=0) - - # b. compute expectation wrt labor market shock - V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) - Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) - - # b. compute expectation wrt productivity - V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) - Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) - - # d. consumption today on tomorrow's grid and endogenous asset grid today - W = beta * V_p2 - uc_nextgrid = beta * Va_p2 - c_nextgrid = uc_nextgrid ** (-eis) - a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) - - # e. upper envelope - imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region - V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) - - # f. update Va - uc = c ** (-1 / eis) - Va = (1 + rpost) * uc - - # PART 2: things we need for GE - - # 2/a. asset policy - a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c - - # 2/b. choice probabilities (don't need jacobian) - P = np.zeros((Nx, Ns, Nz, Na)) - for ix in range(Nx): - V_ix = np.take(V, indices=choice_set[ix], axis=0) - P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) - - # 2/c. average productivity of employed - ze = np.zeros_like(a) - ze[0, ...] = z_grid[:, np.newaxis] - - # 2/d. UI claims - ui = np.zeros_like(a) - ui[1, ...] = b_grid[:, np.newaxis] - - return V, Va, a, c, P, ze, ui - - -def labor_income(z_grid, b_grid, atw, transfer, expiry, fU, fN, s): - # 1. income - yE = atw * z_grid + transfer - yUb = atw * b_grid + transfer - yN = np.zeros_like(yE) + transfer - y_grid = np.vstack((yE, yUb, yN, yN)) - - # 2. transition matrix for labor market status - Pi_s = np.array([[1 - s, s, 0], [fU, (1 - fU) * (1 - expiry), (1 - fU) * expiry], [fU, 0, 1 - fU], [fN, 0, 1 - fN]]) - - return Pi_s, y_grid - - -household.add_hetinput(labor_income, verbose=False) - - -"""Supporting functions for HA block""" - - -@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) -def nonconcave(uc_nextgrid, imin, imax): - """Obtain bounds for non-concave region.""" - nA = uc_nextgrid.shape[-1] - vmin = np.inf - vmax = -np.inf - # step 1: find vmin & vmax - for ia in range(nA - 1): - if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: - vmin_temp = uc_nextgrid[ia] - vmax_temp = uc_nextgrid[ia + 1] - if vmin_temp < vmin: - vmin = vmin_temp - if vmax_temp > vmax: - vmax = vmax_temp - - # 2/a Find imin (upper bound) - if vmin == np.inf: - imin_ = 0 - else: - ia = 0 - while ia < nA: - if uc_nextgrid[ia] < vmin: - break - ia += 1 - imin_ = ia - - # 2/b Find imax (lower bound) - if vmax == -np.inf: - imax_ = nA - else: - ia = nA - while ia > 0: - if uc_nextgrid[ia] > vmax: - break - ia -= 1 - imax_ = ia - - imin[:] = imin_ - imax[:] = imax_ - - -@njit -def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): - """ - Interpolate consumption and value function to exogenous grid. Brute force but safe. - Parameters - ---------- - W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) - a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) - c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) - a_grid : array(Na), exogenous asset grid (tomorrow's grid) - y_grid : array(Ns, Nz), labor income - rpost : float, ex-post interest rate - args : (eis, vphi, chi) arguments for utility function - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function on exogenous grid - c : array(Ns, Nz, Na), consumption on exogenous grid - """ - - # 0. initialize - Ns, Nz, Na = W.shape - c = np.zeros_like(W) - V = -np.inf * np.ones_like(W) - - # outer loop could run in parallel - for iw in range(Ns): - for iz in range(Nz): - ycur = y_grid[iw, iz] - imaxcur = imax[iw, iz] - imincur = imin[iw, iz] - - # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. - # in concave region: exploit monotonicity and don't look for extra solutions - for ia in range(Na): - acur = a_grid[ia] - - # Region 1: below non-concave: exploit monotonicity - if (ia <= imaxcur) | (ia >= imincur): - iap = 0 - ap_low = a_nextgrid[iw, iz, iap] - ap_high = a_nextgrid[iw, iz, iap + 1] - while iap < Na - 2: # can this go up all the way? - if ap_high >= acur: - break - iap += 1 - ap_low = ap_high - ap_high = a_nextgrid[iw, iz, iap + 1] - # found bracket, interpolate value function and consumption - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess - c[iw, iz, ia] = c_guess - - # Region 2: non-concave region - else: - # try out all segments of endogenous grid - for iap in range(Na - 1): - # does this endogenous segment bracket ia? - ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] - interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) - - # does it need to be extrapolated above the endogenous grid? - # if needed to be extrapolated below, we would be in constrained case - extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) - - if interp or extrap_above: - # interpolation slopes - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - - # implied guess - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - - # value - v_guess = util(c_guess, iw, *args) + w_guess - - # select best value for this segment - if v_guess > V[iw, iz, ia]: - V[iw, iz, ia] = v_guess - c[iw, iz, ia] = c_guess - - # 2. constrained case: remember that we have the inverse asset policy a(a') - ia = 0 - while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: - c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur - V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] - ia += 1 - - return V, c - - -'''Simple blocks''' - -@simple -def income_state_vars(mean_z, rho_z, sd_z, nZ, uirate, uicap): - # productivity - z_grid, pi_z, Pi_z = markov_rouwenhorst(rho=rho_z, sigma=sd_z, N=nZ) - z_grid *= mean_z - - # unemployment benefits - b_grid = uirate * z_grid - b_grid[b_grid > uicap] = uicap - return z_grid, b_grid, pi_z, Pi_z - - -@simple -def employment_state_vars(lamM, lamB, lamL): - choice_set = [[0, 3], [1, 3], [2, 3]] - lam_grid = np.array([lamM, lamB, lamL]) - return choice_set, lam_grid - - -@simple -def asset_state_vars(amin, amax, nA): - a_grid = agrid(amin=amin, amax=amax, n=nA) - return a_grid +from sequence_jacobian import simple, create_model, solved +from single_endo import hh as hh_single +from couple_endo import hh as hh_couple '''Rest of the model''' @simple -def firm(Z, K, L, mc, alpha, delta0, delta1, psi): +def firm(Z, K, L, mc, tax, alpha, delta0, delta1, psi): Y = Z * K(-1) ** alpha * L ** (1 - alpha) w = (1 - alpha) * mc * Y / L u = (alpha / delta0 / delta1 * mc * Y / K(-1)) ** (1 / delta1) @@ -344,7 +19,8 @@ def firm(Z, K, L, mc, alpha, delta0, delta1, psi): Q = 1 + psi * (K / K(-1) - 1) I = K - (1 - delta) * K(-1) + psi / 2 * (K / K(-1) - 1) ** 2 * K(-1) transfer = Y - w * L - I - return Y, w, u, delta, Q, I, transfer + atw = (1 - tax) * w + return Y, w, u, delta, Q, I, transfer, atw @simple @@ -368,27 +44,13 @@ def valuation(rpost, mc, Y, K, Q, delta, psi, alpha): @solved(unknowns={'B': [0.0, 10.0]}, targets=['budget'], solver='brentq') -def fiscal1(B, tax, w, rpost, G, Ze, Ui): +def fiscal(B, tax, w, rpost, G, Ze, Ui): budget = (1 + rpost) * B + G + (1 - tax) * w * Ui - tax * w * Ze - B # tax_rule = tax - tax.ss - phi * (B(-1) - B.ss) / Y.ss tax_rule = B - B.ss return budget, tax_rule -@simple -def fiscal2(w, tax): - atw = (1 - tax) * w - return atw - - -@solved(unknowns={'fU': 0.25, 'fN': 0.1, 's': 0.025}, targets=['fU_res', 'fN_res', 's_res'], solver='broyden_custom') -def flows(Y, fU, fN, s, fU_eps, fN_eps, s_eps): - fU_res = fU.ss * (Y / Y.ss) ** fU_eps - fU - fN_res = fN.ss * (Y / Y.ss) ** fN_eps - fN - s_res = s.ss * (Y / Y.ss) ** s_eps - s - return fU_res, fN_res, s_res - - @simple def mkt_clearing(A, B, Y, C, I, G, L, Ze): asset_mkt = A - B @@ -398,31 +60,27 @@ def mkt_clearing(A, B, Y, C, I, G, L, Ze): @simple -def dividends(transfer): - transfer_sm = transfer - transfer_sw = transfer - return transfer_sm, transfer_sw +def dividends(transfer, pop_sm, pop_sw, pop_mc, illiq_sm, illiq_sw, illiq_mc): + transfer_sm = illiq_sm * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + transfer_sw = illiq_sw * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + transfer_mc = illiq_mc * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + return transfer_sm, transfer_sw, transfer_mc @simple -def aggregate(A_sm, C_sm, Ze_sm, Ui_sm, A_sw, C_sw, Ze_sw, Ui_sw): - A = (A_sm + A_sw) / 2 - C = (C_sm + C_sw) / 2 - Ze = (Ze_sm + Ze_sw) / 2 - Ui = (Ui_sm + Ui_sw) / 2 +def aggregate(A_sm, C_sm, Ze_sm, Ui_sm, pop_sm, A_sw, C_sw, Ze_sw, Ui_sw, pop_sw, A_mc, C_mc, Ze_mc, Ui_mc, pop_mc): + A = pop_sm * A_sm + pop_sw * A_sw + pop_mc * A_mc + C = pop_sm * C_sm + pop_sw * C_sw + pop_mc * C_mc + Ze = pop_sm * Ze_sm + pop_sw * Ze_sw + pop_mc * Ze_mc + Ui = pop_sm * Ui_sm + pop_sw * Ui_sw + pop_mc * Ui_mc return A, C, Ze, Ui -@simple -def helper1(A, tax, w, Ze, rpost, Ui): - # after hh block - B = A - G = tax * w * Ze - rpost * B - (1 - tax) * w * Ui - return B, G +'''Steady-state helpers''' @simple -def helper2(eps, rpost, Y, L, alpha, delta0): +def firm_ss(eps, rpost, Y, L, alpha, delta0): # uses u=1 mc = (eps - 1) / eps K = mc * Y * alpha / (rpost + delta0) @@ -432,44 +90,68 @@ def helper2(eps, rpost, Y, L, alpha, delta0): return mc, K, delta1, Z, w -'''Try this''' +@simple +def fiscal_ss(A, tax, w, Ze, rpost, Ui): + # after hh block + B = A + G = tax * w * Ze - rpost * B - (1 - tax) * w * Ui + return B, G + + +'''Calibration''' -cali_sm = {'beta': 0.9782, 'vphi': 0.8079, 'chi': 0.5690, 'fU': 0.25, 'fN': 0.1174, 's': 0.0218, - 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, +cali_sm = {'beta': 0.9833, 'vphi': 0.7625, 'chi': 0.5585, 'fU': 0.2499, 'fN': 0.1148, 's': 0.0203, + 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'illiq': 0.25, 'pop': 0.29, 'fU_eps': 10.69, 'fN_eps': 5.57, 's_eps': -11.17} -cali_sw = {'beta': 0.9830, 'vphi': 0.9909, 'chi': 0.4982, 'fU': 0.22, 'fN': 0.1099, 's': 0.0132, - 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, +cali_sw = {'beta': 0.9830, 'vphi': 0.9940, 'chi': 0.4958, 'fU': 0.2207, 'fN': 0.1098, 's': 0.0132, + 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, 'illiq': 0.15, 'pop': 0.29, 'fU_eps': 8.72, 'fN_eps': 3.55, 's_eps': -6.55} +cali_mc = {'beta': 0.9882, 'illiq': 0.6, 'pop': 0.42, 'rho': 0.042, + 'vphi_m': 0.1545, 'chi_m': 0.2119, 'fU_m': 0.3013, 'fN_m': 0.1840, 's_m': 0.0107, + 'vphi_f': 0.2605, 'chi_f': 0.2477, 'fU_f': 0.2519, 'fN_f': 0.1691, 's_f': 0.0103, + 'mean_m': 1.0, 'rho_m': 0.98, 'sd_m': 0.943*0.82, + 'mean_f': 0.8, 'rho_f': 0.98, 'sd_f': 0.86*0.82, + 'fU_eps_m': 10.37, 'fN_eps_m': 9.74, 's_eps_m': -13.60, + 'fU_eps_f': 8.40, 'fN_eps_f': 2.30, 's_eps_f': -7.87} + + +'''Remap''' + + +to_map_single = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', + 'fU_eps', 'fN_eps', 's_eps', *hh_single.outputs] +hh_sm = hh_single.remap({k: k + '_sm' for k in to_map_single}).rename('SingleMen') +hh_sw = hh_single.remap({k: k + '_sw' for k in to_map_single}).rename('SingleWomen') + +to_map_couple = ['beta', 'transfer', 'rho', + 'vphi_m', 'chi_m', 'fU_m', 'fN_m', 's_m', 'mean_m', 'rho_m', 'sd_m', 'fU_eps_m', 'fN_eps_m', 's_eps_m', + 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f', + *hh_couple.outputs] +hh_mc = hh_couple.remap({k: k + '_mc' for k in to_map_couple}).rename('Couples') + cali_sm = {k + '_sm': v for k, v in cali_sm.items()} cali_sw = {k + '_sw': v for k, v in cali_sw.items()} +cali_mc = {k + '_mc': v for k, v in cali_mc.items()} -calibration = {**cali_sm, **cali_sw, - 'eis': 1.0, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'eps': 10.0, 'tax': 0.3, - 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, 'nZ': 7, - 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0.0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083} - -hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows, household], name='Single') +'''Solve ss''' -to_map = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', 'fU_eps', 'fN_eps', 's_eps', - *hh.outputs] -hh_sm = hh.remap({k: k + '_sm' for k in to_map}).rename('SingleMen') -hh_sw = hh.remap({k: k + '_sw' for k in to_map}).rename('SingleWomen') +hank = create_model([hh_sm, hh_sw, hh_mc, aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], + name='HANK') -hank = create_model([hh_sm, hh_sw, aggregate, dividends, firm, monetary, valuation, nkpc, fiscal1, fiscal2, - mkt_clearing], name='HANK') +calibration = {**cali_sm, **cali_sw, **cali_mc, + 'eis': 1.0, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'eps': 10.0, 'tax': 0.3, + 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, 'nZ': 7, + 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0.0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083} -ss = hank.solve_steady_state(calibration, solver='brentq', dissolve=[fiscal1, flows], - unknowns={'L': (0.7, 0.72), +ss = hank.solve_steady_state(calibration, solver='toms748', dissolve=[fiscal], ttol=1E-6, + unknowns={'L': (1.06, 1.12), 'Z': 0.63, 'mc': 0.9, 'G': 0.2, 'delta1': 1.2, 'B': 3.0, 'K': 17.0}, targets={'labor_mkt': 0.0, 'Y': 1.0, 'nkpc_res': 0.0, 'budget': 0.0, 'asset_mkt': 0.0, 'val': 0.0, 'u': 1.0}, - helper_blocks=[helper1, helper2], + helper_blocks=[firm_ss, fiscal_ss], helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) -out = hh_sm.steady_state(ss) - - diff --git a/tests/single_endo.py b/tests/single_endo.py new file mode 100644 index 0000000..baa3aab --- /dev/null +++ b/tests/single_endo.py @@ -0,0 +1,341 @@ +"""Single household model with frictional labor supply.""" + +import numpy as np +from numba import guvectorize, njit + +from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved +from sequence_jacobian.blocks.discont_block import discont +from sequence_jacobian.utilities.misc import choice_prob, logsum + + +'''Core HA block''' + + +def household_init(a_grid, z_grid, b_grid, atw, transfer, rpost, eis, vphi, chi): + _, income = labor_income(z_grid, b_grid, atw, transfer, 0, 1, 1, 1) + coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + c_guess = 0.3 * coh + V, Va = np.empty_like(coh), np.empty_like(coh) + for iw in range(4): + V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi, chi) + V = V / 0.1 + + # get Va by finite difference + Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) + Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) + Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) + + return Va, V + + +@njit(fastmath=True) +def util(c, iw, eis, vphi, chi): + """Utility function.""" + # 1. utility from consumption. + if eis == 1: + u = np.log(c) + else: + u = c ** (1 - 1 / eis) / (1 - 1 / eis) + + # 2. disutility from work and search + if iw == 0: + u = u - vphi # E + elif (iw == 1) | (iw == 2): + u = u - chi # Ub, U + + return u + + +@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) +def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, z_grid, b_grid, lam_grid, eis, beta, rpost, + vphi, chi): + """ + Backward step function EGM with upper envelope. + + Dimensions: 0: labor market status, 1: productivity, 2: assets. + Status: 0: E, 1: Ub, 2: U, 3: O + State: 0: M, 1: B, 2: L + + Parameters + ---------- + V_p : array(Ns, Nz, Na), status-specific value function tomorrow + Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow + Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks + Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity + choice_set : list(Nz), discrete choices available in each state X + a_grid : array(Na), exogenous asset grid + y_grid : array(Ns, Nz), exogenous labor income grid + z_grid : array(Nz), productivity of employed (need for GE) + b_grid : array(Nz), productivity of unemployed (need for GE) + lam_grid : array(Nx), scale of taste shocks, specific to interim state + eis : float, EIS + beta : float, discount factor + rpost : float, ex-post interest rate + vphi : float, disutility of work + chi : float, disutility of search + + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function today + Va : array(Ns, Nz, Na), partial of status-specific value function today + P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x + c : array(Ns, Nz, Na), status-specific consumption policy today + a : array(Ns, Nz, Na), status-specific asset policy today + ze : array(Ns, Nz, Na), effective labor (average productivity if employed) + ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) + """ + # shapes + Ns, Nz, Na = V_p.shape + Nx = Pi_s_p.shape[1] + + # PART 1: update value and policy functions + # a. discrete choice I expect to make tomorrow + V_p_X = np.empty((Nx, Nz, Na)) + Va_p_X = np.empty((Nx, Nz, Na)) + for ix in range(Nx): + V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) + Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) + P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) + V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) + Va_p_X[ix, ...] = np.sum(P_p_ix * Va_p_ix, axis=0) + + # b. compute expectation wrt labor market shock + V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) + Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) + + # b. compute expectation wrt productivity + V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) + Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) + + # d. consumption today on tomorrow's grid and endogenous asset grid today + W = beta * V_p2 + uc_nextgrid = beta * Va_p2 + c_nextgrid = uc_nextgrid ** (-eis) + a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) + + # e. upper envelope + imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region + V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) + + # f. update Va + uc = c ** (-1 / eis) + Va = (1 + rpost) * uc + + # PART 2: things we need for GE + + # 2/a. asset policy + a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c + + # 2/b. choice probabilities (don't need jacobian) + P = np.zeros((Nx, Ns, Nz, Na)) + for ix in range(Nx): + V_ix = np.take(V, indices=choice_set[ix], axis=0) + P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) + + # 2/c. average productivity of employed + ze = np.zeros_like(a) + ze[0, ...] = z_grid[:, np.newaxis] + + # 2/d. UI claims + ui = np.zeros_like(a) + ui[1, ...] = b_grid[:, np.newaxis] + + return V, Va, a, c, P, ze, ui + + +def labor_income(z_grid, b_grid, atw, transfer, expiry, fU, fN, s): + # 1. income + yE = atw * z_grid + transfer + yUb = atw * b_grid + transfer + yN = np.zeros_like(yE) + transfer + y_grid = np.vstack((yE, yUb, yN, yN)) + + # 2. transition matrix for labor market status + Pi_s = np.array([[1 - s, s, 0], [fU, (1 - fU) * (1 - expiry), (1 - fU) * expiry], [fU, 0, 1 - fU], [fN, 0, 1 - fN]]) + + return Pi_s, y_grid + + +"""Supporting functions for HA block""" + + +@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) +def nonconcave(uc_nextgrid, imin, imax): + """Obtain bounds for non-concave region.""" + nA = uc_nextgrid.shape[-1] + vmin = np.inf + vmax = -np.inf + # step 1: find vmin & vmax + for ia in range(nA - 1): + if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: + vmin_temp = uc_nextgrid[ia] + vmax_temp = uc_nextgrid[ia + 1] + if vmin_temp < vmin: + vmin = vmin_temp + if vmax_temp > vmax: + vmax = vmax_temp + + # 2/a Find imin (upper bound) + if vmin == np.inf: + imin_ = 0 + else: + ia = 0 + while ia < nA: + if uc_nextgrid[ia] < vmin: + break + ia += 1 + imin_ = ia + + # 2/b Find imax (lower bound) + if vmax == -np.inf: + imax_ = nA + else: + ia = nA + while ia > 0: + if uc_nextgrid[ia] > vmax: + break + ia -= 1 + imax_ = ia + + imin[:] = imin_ + imax[:] = imax_ + + +@njit +def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): + """ + Interpolate consumption and value function to exogenous grid. Brute force but safe. + Parameters + ---------- + W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) + a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) + c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) + a_grid : array(Na), exogenous asset grid (tomorrow's grid) + y_grid : array(Ns, Nz), labor income + rpost : float, ex-post interest rate + args : (eis, vphi, chi) arguments for utility function + Returns + ------- + V : array(Ns, Nz, Na), status-specific value function on exogenous grid + c : array(Ns, Nz, Na), consumption on exogenous grid + """ + + # 0. initialize + Ns, Nz, Na = W.shape + c = np.zeros_like(W) + V = -np.inf * np.ones_like(W) + + # outer loop could run in parallel + for iw in range(Ns): + for iz in range(Nz): + ycur = y_grid[iw, iz] + imaxcur = imax[iw, iz] + imincur = imin[iw, iz] + + # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. + # in concave region: exploit monotonicity and don't look for extra solutions + for ia in range(Na): + acur = a_grid[ia] + + # Region 1: below non-concave: exploit monotonicity + if (ia <= imaxcur) | (ia >= imincur): + iap = 0 + ap_low = a_nextgrid[iw, iz, iap] + ap_high = a_nextgrid[iw, iz, iap + 1] + while iap < Na - 2: # can this go up all the way? + if ap_high >= acur: + break + iap += 1 + ap_low = ap_high + ap_high = a_nextgrid[iw, iz, iap + 1] + # found bracket, interpolate value function and consumption + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess + c[iw, iz, ia] = c_guess + + # Region 2: non-concave region + else: + # try out all segments of endogenous grid + for iap in range(Na - 1): + # does this endogenous segment bracket ia? + ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] + interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) + + # does it need to be extrapolated above the endogenous grid? + # if needed to be extrapolated below, we would be in constrained case + extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) + + if interp or extrap_above: + # interpolation slopes + w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] + c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] + w_slope = (w_high - w_low) / (ap_high - ap_low) + c_slope = (c_high - c_low) / (ap_high - ap_low) + + # implied guess + c_guess = c_low + c_slope * (acur - ap_low) + w_guess = w_low + w_slope * (acur - ap_low) + + # value + v_guess = util(c_guess, iw, *args) + w_guess + + # select best value for this segment + if v_guess > V[iw, iz, ia]: + V[iw, iz, ia] = v_guess + c[iw, iz, ia] = c_guess + + # 2. constrained case: remember that we have the inverse asset policy a(a') + ia = 0 + while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: + c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur + V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] + ia += 1 + + return V, c + + +'''Simple blocks''' + + +@simple +def income_state_vars(mean_z, rho_z, sd_z, nZ, uirate, uicap): + # productivity + z_grid, pi_z, Pi_z = markov_rouwenhorst(rho=rho_z, sigma=sd_z, N=nZ) + z_grid *= mean_z + + # unemployment benefits + b_grid = uirate * z_grid + b_grid[b_grid > uicap] = uicap + return z_grid, b_grid, pi_z, Pi_z + + +@simple +def employment_state_vars(lamM, lamB, lamL): + choice_set = [[0, 3], [1, 3], [2, 3]] + lam_grid = np.array([lamM, lamB, lamL]) + return choice_set, lam_grid + + +@simple +def asset_state_vars(amin, amax, nA): + a_grid = agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +@solved(unknowns={'fU': 0.25, 'fN': 0.1, 's': 0.025}, targets=['fU_res', 'fN_res', 's_res'], solver='broyden_custom') +def flows(Y, fU, fN, s, fU_eps, fN_eps, s_eps): + fU_res = fU.ss * (Y / Y.ss) ** fU_eps - fU + fN_res = fN.ss * (Y / Y.ss) ** fN_eps - fN + s_res = s.ss * (Y / Y.ss) ** s_eps - s + return fU_res, fN_res, s_res + + +'''Put it together''' + +household.add_hetinput(labor_income, verbose=False) +hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows, household], name='SingleHH') From 737aa3abdc393c29f44c6df59585ea677b149ceb Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 25 Jun 2021 15:01:27 -0500 Subject: [PATCH 184/288] Complete Bijection's `__matmul__` methods with `NotImplemented`. --- .../blocks/support/impulse.py | 26 ++++++++++++++----- src/sequence_jacobian/steady_state/classes.py | 2 ++ tests/base/test_public_classes.py | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index c8fe87f..345363f 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -35,40 +35,52 @@ def __getitem__(self, item): if isinstance(item, str): # Case 1: ImpulseDict['C'] returns array return self.impulse[item] - if isinstance(item, list): + elif isinstance(item, list): # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts return type(self)({k: self.impulse[k] for k in item}) + else: + ValueError("Use ImpulseDict['X'] to return an array or ImpulseDict[['X']] to return a smaller ImpulseDict.") def __add__(self, other): if isinstance(other, (float, int)): return type(self)({k: v + other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): + elif isinstance(other, SteadyStateDict): return type(self)({k: v + other[k] for k, v in self.impulse.items()}) + else: + NotImplementedError('Only a number or a SteadyStateDict can be added from an ImpulseDict.') def __sub__(self, other): if isinstance(other, (float, int)): return type(self)({k: v - other for k, v in self.impulse.items()}) - if isinstance(other, (SteadyStateDict, ImpulseDict)): + elif isinstance(other, (SteadyStateDict, ImpulseDict)): return type(self)({k: v - other[k] for k, v in self.impulse.items()}) + else: + NotImplementedError('Only a number or a SteadyStateDict can be subtracted from an ImpulseDict.') def __mul__(self, other): if isinstance(other, (float, int)): return type(self)({k: v * other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): + elif isinstance(other, SteadyStateDict): return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + else: + NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __rmul__(self, other): if isinstance(other, (float, int)): return type(self)({k: v * other for k, v in self.impulse.items()}) - if isinstance(other, SteadyStateDict): + elif isinstance(other, SteadyStateDict): return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + else: + NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __truediv__(self, other): if isinstance(other, (float, int)): return type(self)({k: v / other for k, v in self.impulse.items()}) # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero - if isinstance(other, SteadyStateDict): + elif isinstance(other, SteadyStateDict): return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) + else: + NotImplementedError('An ImpulseDict can only be divided by a number or a SteadyStateDict.') def __matmul__(self, x): # remap keys in toplevel @@ -76,6 +88,8 @@ def __matmul__(self, x): new = deepcopy(self) new.impulse = x @ self.impulse return new + else: + NotImplemented def __rmatmul__(self, x): return self.__matmul__(x) diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index e4ff000..c281b74 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -39,6 +39,8 @@ def __matmul__(self, x): new = deepcopy(self) new.toplevel = x @ self.toplevel return new + else: + NotImplemented def __rmatmul__(self, x): return self.__matmul__(x) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 1c325e8..ceee3d3 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -91,7 +91,7 @@ def test_bijection(): mymap2 = Bijection({'a1': 'a2'}) assert (mymap2 @ mymap)['a'] == 'a2' - # composition with SteadyStateDict (only left __matmul__ works as intended) + # composition with SteadyStateDict ss = SteadyStateDict({'a': 2.0, 'b': 1.0}, internal={}) ss_remapped = ss @ mymap assert isinstance(ss_remapped, SteadyStateDict) From 28c2987c5e250326e724695987945be6e71e1d3b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 25 Jun 2021 16:41:37 -0500 Subject: [PATCH 185/288] Could .solve_steady_state return internals nested in CombinedBlocks? Can use remap_test.py to play with this. --- tests/dcblock_test.py | 60 +++++++++++++++++++++++++++++-------------- tests/remap_test.py | 16 +++++++----- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/tests/dcblock_test.py b/tests/dcblock_test.py index 4e59229..026adb7 100644 --- a/tests/dcblock_test.py +++ b/tests/dcblock_test.py @@ -3,8 +3,8 @@ import numpy as np from sequence_jacobian import simple, create_model, solved -from single_endo import hh as hh_single -from couple_endo import hh as hh_couple +import single_endo as single +import couple_endo as couple '''Rest of the model''' @@ -101,39 +101,48 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): '''Calibration''' cali_sm = {'beta': 0.9833, 'vphi': 0.7625, 'chi': 0.5585, 'fU': 0.2499, 'fN': 0.1148, 's': 0.0203, - 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, 'illiq': 0.25, 'pop': 0.29, - 'fU_eps': 10.69, 'fN_eps': 5.57, 's_eps': -11.17} + 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, + 'fU_eps': 10.69, 'fN_eps': 5.57, 's_eps': -11.17, + 'illiq': 0.25, 'pop': 0.29} cali_sw = {'beta': 0.9830, 'vphi': 0.9940, 'chi': 0.4958, 'fU': 0.2207, 'fN': 0.1098, 's': 0.0132, - 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, 'illiq': 0.15, 'pop': 0.29, - 'fU_eps': 8.72, 'fN_eps': 3.55, 's_eps': -6.55} + 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, + 'fU_eps': 8.72, 'fN_eps': 3.55, 's_eps': -6.55, + 'illiq': 0.15, 'pop': 0.29} -cali_mc = {'beta': 0.9882, 'illiq': 0.6, 'pop': 0.42, 'rho': 0.042, +cali_mc = {'beta': 0.9882, 'rho': 0.042, 'vphi_m': 0.1545, 'chi_m': 0.2119, 'fU_m': 0.3013, 'fN_m': 0.1840, 's_m': 0.0107, 'vphi_f': 0.2605, 'chi_f': 0.2477, 'fU_f': 0.2519, 'fN_f': 0.1691, 's_f': 0.0103, 'mean_m': 1.0, 'rho_m': 0.98, 'sd_m': 0.943*0.82, 'mean_f': 0.8, 'rho_f': 0.98, 'sd_f': 0.86*0.82, 'fU_eps_m': 10.37, 'fN_eps_m': 9.74, 's_eps_m': -13.60, - 'fU_eps_f': 8.40, 'fN_eps_f': 2.30, 's_eps_f': -7.87} - + 'fU_eps_f': 8.40, 'fN_eps_f': 2.30, 's_eps_f': -7.87, + 'illiq': 0.6, 'pop': 0.42} '''Remap''' - +# variables to remap to_map_single = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', - 'fU_eps', 'fN_eps', 's_eps', *hh_single.outputs] -hh_sm = hh_single.remap({k: k + '_sm' for k in to_map_single}).rename('SingleMen') -hh_sw = hh_single.remap({k: k + '_sw' for k in to_map_single}).rename('SingleWomen') - + 'fU_eps', 'fN_eps', 's_eps', *single.hh.outputs] to_map_couple = ['beta', 'transfer', 'rho', 'vphi_m', 'chi_m', 'fU_m', 'fN_m', 's_m', 'mean_m', 'rho_m', 'sd_m', 'fU_eps_m', 'fN_eps_m', 's_eps_m', 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f', - *hh_couple.outputs] -hh_mc = hh_couple.remap({k: k + '_mc' for k in to_map_couple}).rename('Couples') + *couple.hh.outputs] -cali_sm = {k + '_sm': v for k, v in cali_sm.items()} -cali_sw = {k + '_sw': v for k, v in cali_sw.items()} -cali_mc = {k + '_mc': v for k, v in cali_mc.items()} +# Single men +hh_sm = create_model([single.income_state_vars, single.employment_state_vars, single.asset_state_vars, single.flows, + single.household.rename('single_men')], name='SingleMen') +hh_sm = hh_sm.remap({k: k + '_sm' for k in to_map_single}) + +# Single women +hh_sw = create_model([single.income_state_vars, single.employment_state_vars, single.asset_state_vars, single.flows, + single.household.rename('single_women')], name='SingleWomen') +hh_sw = hh_sw.remap({k: k + '_sw' for k in to_map_single}) + +# Married couples +hh_mc = create_model([couple.income_state_vars, couple.employment_state_vars, couple.asset_state_vars, + couple.flows_m, couple.flows_f, couple.household.rename('couples')], name='Couples') +hh_mc = hh_mc.remap({k: k + '_mc' for k in to_map_couple}) '''Solve ss''' @@ -142,6 +151,11 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): hank = create_model([hh_sm, hh_sw, hh_mc, aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], name='HANK') +# remap calibration +cali_sm = {k + '_sm': v for k, v in cali_sm.items()} +cali_sw = {k + '_sw': v for k, v in cali_sw.items()} +cali_mc = {k + '_mc': v for k, v in cali_mc.items()} + calibration = {**cali_sm, **cali_sw, **cali_mc, 'eis': 1.0, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'eps': 10.0, 'tax': 0.3, 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, 'nZ': 7, @@ -155,3 +169,11 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): helper_blocks=[firm_ss, fiscal_ss], helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) + +# jacobians +# J = {} +# J['sm'] = hh_sm.jacobian(ss, exogenous=['atw', 'rpost', 'beta_sm', 'Y', 'transfer_sm'], T=500) + +# td_nonlin = hank.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, +# unknowns=['K'], targets=['asset_mkt']) + diff --git a/tests/remap_test.py b/tests/remap_test.py index 0419171..221c6d3 100644 --- a/tests/remap_test.py +++ b/tests/remap_test.py @@ -115,9 +115,13 @@ def mpcs(c, a, a_grid, r): # remap method takes a dict and returns new copies of blocks household.add_hetoutput(mpcs, verbose=False) -to_map = ['beta', *household.outputs] -hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('patient household') -hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('impatient household') +hh_patient = create_model([income_state_vars, household.rename('patient_hh')], name='PatientHH') +to_map = ['beta', *hh_patient.outputs] + +hh_patient = hh_patient.remap({k: k + '_patient' for k in to_map}) + +hh_impatient = create_model([income_state_vars, household.rename('impatient_hh')], name='ImpatientHH') +hh_impatient = hh_impatient.remap({k: k + '_impatient' for k in to_map}) @simple @@ -131,7 +135,7 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i '''Steady state''' # DAG -blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] +blocks = [hh_patient, hh_impatient, firm, mkt_clearing, asset_state_vars, aggregate] ks_model = create_model(blocks, name="Krusell-Smith") # Steady State @@ -142,5 +146,5 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, helper_blocks=[firm_ss], helper_targets=['Y', 'r']) -td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, - unknowns=['K'], targets=['asset_mkt']) +# td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, +# unknowns=['K'], targets=['asset_mkt']) From 82e16595d6ed6abc82cd5964a6c2a4cc049422b3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 8 Jul 2021 11:01:41 -0500 Subject: [PATCH 186/288] Return empty JacobianDict if relevant_shocks = [] for SolvedBlocks --- src/sequence_jacobian/blocks/solved_block.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 3a2caa7..fc05109 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -4,6 +4,8 @@ from ..blocks.simple_block import simple from ..utilities import graph +from ..jacobian.classes import JacobianDict + def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: @@ -121,6 +123,9 @@ def jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): outputs = list(self.outputs) relevant_shocks = [i for i in self.inputs if i in exogenous] - return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, outputs=outputs, Js=Js) + if relevant_shocks: + return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), + targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + T=T, outputs=outputs, Js=Js) + else: + return JacobianDict({}) From 781f786f1ff8a118eeaddfc7c8a6c8ac0fb234c1 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 15 Jul 2021 19:18:32 -0400 Subject: [PATCH 187/288] save temporary changes --- src/sequence_jacobian/primitives.py | 2 ++ tests/couple_endo.py | 1 + tests/dcblock_test.py | 48 ++++++++++++++--------------- tests/remap_test.py | 16 ++++------ tests/single_endo.py | 1 + 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index c8201ad..042e1c1 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -151,6 +151,8 @@ def remap(self, map): other.input_list = other.M @ self.input_list if hasattr(self, 'output_list'): other.output_list = other.M @ self.output_list + if hasattr(self, 'non_back_iter_outputs'): + other.non_back_iter_outputs = other.M @ self.non_back_iter_outputs return other def rename(self, name): diff --git a/tests/couple_endo.py b/tests/couple_endo.py index b70df73..5aee8f4 100644 --- a/tests/couple_endo.py +++ b/tests/couple_endo.py @@ -402,3 +402,4 @@ def flows_f(Y, fU_f, fN_f, s_f, fU_eps_f, fN_eps_f, s_eps_f): household.add_hetinput(labor_income, verbose=False) hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows_m, flows_f, household], name='CoupleHH') +blocks = [income_state_vars, employment_state_vars, asset_state_vars, flows_m, flows_f, household] diff --git a/tests/dcblock_test.py b/tests/dcblock_test.py index 026adb7..637dd99 100644 --- a/tests/dcblock_test.py +++ b/tests/dcblock_test.py @@ -32,7 +32,8 @@ def monetary(pi, rstar, phi_pi): @simple def nkpc(pi, mc, eps, Y, rpost, kappa): - nkpc_res = kappa * (mc - (eps - 1) / eps) + Y(+1) / Y * np.log(1 + pi(+1)) / (1 + rpost(+1)) - np.log(1 + pi) + nkpc_res = kappa * (mc - (eps - 1) / eps) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + rpost(+1)) \ + - (1 + pi).apply(np.log) return nkpc_res @@ -123,32 +124,25 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): # variables to remap to_map_single = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', - 'fU_eps', 'fN_eps', 's_eps', *single.hh.outputs] + 'fU_eps', 'fN_eps', 's_eps'] + list(single.hh.outputs) + to_map_couple = ['beta', 'transfer', 'rho', 'vphi_m', 'chi_m', 'fU_m', 'fN_m', 's_m', 'mean_m', 'rho_m', 'sd_m', 'fU_eps_m', 'fN_eps_m', 's_eps_m', - 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f', - *couple.hh.outputs] - -# Single men -hh_sm = create_model([single.income_state_vars, single.employment_state_vars, single.asset_state_vars, single.flows, - single.household.rename('single_men')], name='SingleMen') -hh_sm = hh_sm.remap({k: k + '_sm' for k in to_map_single}) + 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f'] + list(couple.hh.outputs) -# Single women -hh_sw = create_model([single.income_state_vars, single.employment_state_vars, single.asset_state_vars, single.flows, - single.household.rename('single_women')], name='SingleWomen') -hh_sw = hh_sw.remap({k: k + '_sw' for k in to_map_single}) +# Singles +blocks_sm = [b.remap({k: k + '_sm' for k in to_map_single}).rename(b.name + '_sm') for b in single.blocks] +blocks_sw = [b.remap({k: k + '_sw' for k in to_map_single}).rename(b.name + '_sw') for b in single.blocks] -# Married couples -hh_mc = create_model([couple.income_state_vars, couple.employment_state_vars, couple.asset_state_vars, - couple.flows_m, couple.flows_f, couple.household.rename('couples')], name='Couples') -hh_mc = hh_mc.remap({k: k + '_mc' for k in to_map_couple}) +# Couples +blocks_mc = [b.remap({k: k + '_mc' for k in to_map_couple}).rename(b.name + '_mc') for b in couple.blocks] '''Solve ss''' -hank = create_model([hh_sm, hh_sw, hh_mc, aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], +hank = create_model(blocks_sm + blocks_sw + blocks_mc + + [aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], name='HANK') # remap calibration @@ -170,10 +164,14 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) -# jacobians -# J = {} -# J['sm'] = hh_sm.jacobian(ss, exogenous=['atw', 'rpost', 'beta_sm', 'Y', 'transfer_sm'], T=500) - -# td_nonlin = hank.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, -# unknowns=['K'], targets=['asset_mkt']) - +# # jacobians +# J = dict() +# J['household_sm'] = blocks_sm[4].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', 'fU', 'fN', 's'], T=500) +# J['household_sw'] = blocks_sw[4].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', 'fU', 'fN', 's'], T=500) +# J['household_mc'] = blocks_mc[5].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', +# 'fU_m', 'fN_m', 's_m', 'fU_f', 'fN_f', 's_f'], T=500) +# +# td_lin = hank.solve_impulse_linear(ss, {'rstar': 0.001*0.9**np.arange(500)}, +# unknowns=['K', 'L', 'mc', 'pi', 'tax'], +# targets=['val', 'goods_mkt', 'labor_mkt', 'nkpc_res', 'tax_rule'], +# Js=J) diff --git a/tests/remap_test.py b/tests/remap_test.py index 221c6d3..0419171 100644 --- a/tests/remap_test.py +++ b/tests/remap_test.py @@ -115,13 +115,9 @@ def mpcs(c, a, a_grid, r): # remap method takes a dict and returns new copies of blocks household.add_hetoutput(mpcs, verbose=False) -hh_patient = create_model([income_state_vars, household.rename('patient_hh')], name='PatientHH') -to_map = ['beta', *hh_patient.outputs] - -hh_patient = hh_patient.remap({k: k + '_patient' for k in to_map}) - -hh_impatient = create_model([income_state_vars, household.rename('impatient_hh')], name='ImpatientHH') -hh_impatient = hh_impatient.remap({k: k + '_impatient' for k in to_map}) +to_map = ['beta', *household.outputs] +hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('patient household') +hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('impatient household') @simple @@ -135,7 +131,7 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i '''Steady state''' # DAG -blocks = [hh_patient, hh_impatient, firm, mkt_clearing, asset_state_vars, aggregate] +blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] ks_model = create_model(blocks, name="Krusell-Smith") # Steady State @@ -146,5 +142,5 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, helper_blocks=[firm_ss], helper_targets=['Y', 'r']) -# td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, -# unknowns=['K'], targets=['asset_mkt']) +td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, + unknowns=['K'], targets=['asset_mkt']) diff --git a/tests/single_endo.py b/tests/single_endo.py index baa3aab..13dc9e2 100644 --- a/tests/single_endo.py +++ b/tests/single_endo.py @@ -339,3 +339,4 @@ def flows(Y, fU, fN, s, fU_eps, fN_eps, s_eps): household.add_hetinput(labor_income, verbose=False) hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows, household], name='SingleHH') +blocks = [income_state_vars, employment_state_vars, asset_state_vars, flows, household] From b8af179e2798cb164af4338e7cf54ef9eeafc928 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 21 Jul 2021 13:59:54 -0400 Subject: [PATCH 188/288] cleaned up tests/; having other scripts upset VS code --- .gitignore | 3 +- tests/couple_endo.py => couple_endo.py | 0 tests/dcblock_test.py => dcblock_test.py | 34 +++-- hank_fiscal.py | 172 +++++++++++++++++++++++ tests/remap_test.py => remap_test.py | 0 tests/single_endo.py => single_endo.py | 0 6 files changed, 193 insertions(+), 16 deletions(-) rename tests/couple_endo.py => couple_endo.py (100%) rename tests/dcblock_test.py => dcblock_test.py (85%) create mode 100644 hank_fiscal.py rename tests/remap_test.py => remap_test.py (100%) rename tests/single_endo.py => single_endo.py (100%) diff --git a/.gitignore b/.gitignore index f6ec28d..4526876 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ __pycache__/ .ipynb_checkpoints/ .DS_Store -.vscode +.vscode/ +.pytest_cache/ *.zip diff --git a/tests/couple_endo.py b/couple_endo.py similarity index 100% rename from tests/couple_endo.py rename to couple_endo.py diff --git a/tests/dcblock_test.py b/dcblock_test.py similarity index 85% rename from tests/dcblock_test.py rename to dcblock_test.py index 637dd99..cf7df30 100644 --- a/tests/dcblock_test.py +++ b/dcblock_test.py @@ -33,14 +33,14 @@ def monetary(pi, rstar, phi_pi): @simple def nkpc(pi, mc, eps, Y, rpost, kappa): nkpc_res = kappa * (mc - (eps - 1) / eps) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + rpost(+1)) \ - - (1 + pi).apply(np.log) + - (1 + pi).apply(np.log) return nkpc_res @simple def valuation(rpost, mc, Y, K, Q, delta, psi, alpha): val = alpha * mc(+1) * Y(+1) / K - (K(+1) / K - (1 - delta) + psi / 2 * (K(+1) / K - 1) ** 2) + \ - K(+1) / K * Q(+1) - (1 + rpost(+1)) * Q + K(+1) / K * Q(+1) - (1 + rpost(+1)) * Q return val @@ -62,9 +62,12 @@ def mkt_clearing(A, B, Y, C, I, G, L, Ze): @simple def dividends(transfer, pop_sm, pop_sw, pop_mc, illiq_sm, illiq_sw, illiq_mc): - transfer_sm = illiq_sm * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) - transfer_sw = illiq_sw * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) - transfer_mc = illiq_mc * transfer / (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + transfer_sm = illiq_sm * transfer / \ + (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + transfer_sw = illiq_sw * transfer / \ + (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) + transfer_mc = illiq_mc * transfer / \ + (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) return transfer_sm, transfer_sw, transfer_mc @@ -124,26 +127,27 @@ def fiscal_ss(A, tax, w, Ze, rpost, Ui): # variables to remap to_map_single = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', - 'fU_eps', 'fN_eps', 's_eps'] + list(single.hh.outputs) + 'fU_eps', 'fN_eps', 's_eps', *single.hh.outputs] to_map_couple = ['beta', 'transfer', 'rho', 'vphi_m', 'chi_m', 'fU_m', 'fN_m', 's_m', 'mean_m', 'rho_m', 'sd_m', 'fU_eps_m', 'fN_eps_m', 's_eps_m', - 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f'] + list(couple.hh.outputs) + 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f', + *couple.hh.outputs] -# Singles -blocks_sm = [b.remap({k: k + '_sm' for k in to_map_single}).rename(b.name + '_sm') for b in single.blocks] -blocks_sw = [b.remap({k: k + '_sw' for k in to_map_single}).rename(b.name + '_sw') for b in single.blocks] - -# Couples -blocks_mc = [b.remap({k: k + '_mc' for k in to_map_couple}).rename(b.name + '_mc') for b in couple.blocks] +# remap blocks one-by-one (replicates combinedblock) +blocks_sm = [b.remap({k: k + '_sm' for k in to_map_single} + ).rename(b.name + '_sm') for b in single.blocks] +blocks_sw = [b.remap({k: k + '_sw' for k in to_map_single} + ).rename(b.name + '_sw') for b in single.blocks] +blocks_mc = [b.remap({k: k + '_mc' for k in to_map_couple} + ).rename(b.name + '_mc') for b in couple.blocks] '''Solve ss''' hank = create_model(blocks_sm + blocks_sw + blocks_mc + - [aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], - name='HANK') + [aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], name='HANK') # remap calibration cali_sm = {k + '_sm': v for k, v in cali_sm.items()} diff --git a/hank_fiscal.py b/hank_fiscal.py new file mode 100644 index 0000000..7510d55 --- /dev/null +++ b/hank_fiscal.py @@ -0,0 +1,172 @@ +import numpy as np +import sequence_jacobian as sj +from sequence_jacobian.blocks.support.bijection import Bijection +from sequence_jacobian.steady_state.classes import SteadyStateDict + + +'''Part 1: Household block''' + + +def household_init(a_grid, y, rpost, sigma): + c = np.maximum(1e-8, y[:, np.newaxis] + np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) + Va = (1 + rpost) * (c ** (-sigma)) + return Va + + +@sj.het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, Pi_p, a_grid, y, rpost, beta, sigma): + """ + Backward step in simple incomplete market model. Assumes CRRA utility. + + Parameters + ---------- + Va_p : array (E, A), marginal value of assets tomorrow (backward iterator) + Pi_p : array (E, E), Markov matrix for skills tomorrow + a_grid : array (A), asset grid + y : array (E), non-financial income + rpost : scalar, ex-post return on assets + beta : scalar, discount factor + sigma : scalar, utility parameter + + Returns + ------- + Va : array (E, A), marginal value of assets today + a : array (E, A), asset policy today + c : array (E, A), consumption policy today + """ + uc_nextgrid = (beta * Pi_p) @ Va_p + c_nextgrid = uc_nextgrid ** (-1 / sigma) + coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] + a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) + sj.utilities.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + uc = c ** (-sigma) + Va = (1 + rpost) * uc + + return Va, a, c + + +def get_mpcs(c, a, a_grid, rpost): + """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" + mpcs_ = np.empty_like(c) + post_return = (1 + rpost) * a_grid + + # symmetric differences away from boundaries + mpcs_[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (post_return[2:] - post_return[:-2]) + + # asymmetric first differences at boundaries + mpcs_[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) + mpcs_[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) + + # special case of constrained + mpcs_[a == a_grid[0]] = 1 + + return mpcs_ + + +def income(tau, Y, e_grid, e_dist, Gamma, transfer): + """Labor income on the grid.""" + gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, e_grid ** (1 + Gamma * np.log(Y))) + y = (1 - tau) * Y * gamma * e_grid + transfer + return y + + +@sj.simple +def income_state_vars(rho_e, sd_e, nE): + e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) + return e_grid, e_dist, Pi + + +@sj.simple +def asset_state_vars(amin, amax, nA): + a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +@sj.hetoutput() +def mpcs(c, a, a_grid, rpost): + """MPC out of lump-sum transfer.""" + mpc = get_mpcs(c, a, a_grid, rpost) + return mpc + + +household.add_hetinput(income, verbose=False) +household.add_hetoutput(mpcs, verbose=False) + + +'''Part 2: rest of the model''' + + +@sj.simple +def interest_rates(r): + rpost = r(-1) # household ex-post return + rb = r(-1) # rate on 1-period real bonds + return rpost, rb + + +@sj.solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +def fiscal(B, G, rb, rho_B, Y, transfer): + B_rule = B.ss + rho_B * (B(-1) - B.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau + + +@sj.simple +def fiscal_dis(B, G, rb, rho_B, Y, transfer): + B_rule = B.ss + rho_B * (B(-1) - B.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau + + +@sj.simple +def mkt_clearing(A, B, C, Y, G): + asset_mkt = A - B + goods_mkt = Y - C - G + return asset_mkt, goods_mkt + + +'''Try simple block''' + + +@sj.simple +def alma(a, b): + c = a + b + return c + + +alma2 = alma.remap({k: k + '_out' for k in ['a', 'b', 'c']}) +cali2 = SteadyStateDict({'a_out': 1, 'b_out': 2}, internal={}) +ss2 = alma2.steady_state(cali2) +jac2 = alma2.jacobian(cali2, ['a_out']) + +mymap = Bijection({'a': 'A', 'c': 'C'}) + +cali = SteadyStateDict({'a': 1, 'b': 2}, internal={}) +jac = alma.jacobian(cali, ['a']) + + +'''More serious''' + +# household_remapped = household.remap({'beta': 'beta1', 'Mpc': 'Mpc1'}) +# hh = sj.create_model([household_remapped, income_state_vars, asset_state_vars], name='Household') +# dag = sj.create_model([hh, interest_rates, fiscal_dis, mkt_clearing], name='HANK') +# +# calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 11, +# 'amin': 0.0, 'amax': 1000, 'nA': 500, 'Gamma': 0.0, 'rho_B': 0.9, 'transfer': 0.143} +# +# ss = dag.solve_steady_state(calibration, +# unknowns={'beta1': .95, 'G': 0.2, 'B': 2.0}, +# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc1': 0.25}, +# solver='hybr') + +# td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, +# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) +# +# +# td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, +# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) + + + diff --git a/tests/remap_test.py b/remap_test.py similarity index 100% rename from tests/remap_test.py rename to remap_test.py diff --git a/tests/single_endo.py b/single_endo.py similarity index 100% rename from tests/single_endo.py rename to single_endo.py From f00c2b52ecf250d1ef1b361313a7d18eba84bd35 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 21 Jul 2021 17:52:16 -0400 Subject: [PATCH 189/288] Test case for nested CombinedBlocks: remap_test.py --- hank_fiscal.py | 172 ------------------------------------------------- remap_test.py | 25 ++++--- 2 files changed, 17 insertions(+), 180 deletions(-) delete mode 100644 hank_fiscal.py diff --git a/hank_fiscal.py b/hank_fiscal.py deleted file mode 100644 index 7510d55..0000000 --- a/hank_fiscal.py +++ /dev/null @@ -1,172 +0,0 @@ -import numpy as np -import sequence_jacobian as sj -from sequence_jacobian.blocks.support.bijection import Bijection -from sequence_jacobian.steady_state.classes import SteadyStateDict - - -'''Part 1: Household block''' - - -def household_init(a_grid, y, rpost, sigma): - c = np.maximum(1e-8, y[:, np.newaxis] + np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) - Va = (1 + rpost) * (c ** (-sigma)) - return Va - - -@sj.het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, y, rpost, beta, sigma): - """ - Backward step in simple incomplete market model. Assumes CRRA utility. - - Parameters - ---------- - Va_p : array (E, A), marginal value of assets tomorrow (backward iterator) - Pi_p : array (E, E), Markov matrix for skills tomorrow - a_grid : array (A), asset grid - y : array (E), non-financial income - rpost : scalar, ex-post return on assets - beta : scalar, discount factor - sigma : scalar, utility parameter - - Returns - ------- - Va : array (E, A), marginal value of assets today - a : array (E, A), asset policy today - c : array (E, A), consumption policy today - """ - uc_nextgrid = (beta * Pi_p) @ Va_p - c_nextgrid = uc_nextgrid ** (-1 / sigma) - coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] - a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) - sj.utilities.optimized_routines.setmin(a, a_grid[0]) - c = coh - a - uc = c ** (-sigma) - Va = (1 + rpost) * uc - - return Va, a, c - - -def get_mpcs(c, a, a_grid, rpost): - """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" - mpcs_ = np.empty_like(c) - post_return = (1 + rpost) * a_grid - - # symmetric differences away from boundaries - mpcs_[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (post_return[2:] - post_return[:-2]) - - # asymmetric first differences at boundaries - mpcs_[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) - mpcs_[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) - - # special case of constrained - mpcs_[a == a_grid[0]] = 1 - - return mpcs_ - - -def income(tau, Y, e_grid, e_dist, Gamma, transfer): - """Labor income on the grid.""" - gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, e_grid ** (1 + Gamma * np.log(Y))) - y = (1 - tau) * Y * gamma * e_grid + transfer - return y - - -@sj.simple -def income_state_vars(rho_e, sd_e, nE): - e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) - return e_grid, e_dist, Pi - - -@sj.simple -def asset_state_vars(amin, amax, nA): - a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) - return a_grid - - -@sj.hetoutput() -def mpcs(c, a, a_grid, rpost): - """MPC out of lump-sum transfer.""" - mpc = get_mpcs(c, a, a_grid, rpost) - return mpc - - -household.add_hetinput(income, verbose=False) -household.add_hetoutput(mpcs, verbose=False) - - -'''Part 2: rest of the model''' - - -@sj.simple -def interest_rates(r): - rpost = r(-1) # household ex-post return - rb = r(-1) # rate on 1-period real bonds - return rpost, rb - - -@sj.solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal(B, G, rb, rho_B, Y, transfer): - B_rule = B.ss + rho_B * (B(-1) - B.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau - - -@sj.simple -def fiscal_dis(B, G, rb, rho_B, Y, transfer): - B_rule = B.ss + rho_B * (B(-1) - B.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau - - -@sj.simple -def mkt_clearing(A, B, C, Y, G): - asset_mkt = A - B - goods_mkt = Y - C - G - return asset_mkt, goods_mkt - - -'''Try simple block''' - - -@sj.simple -def alma(a, b): - c = a + b - return c - - -alma2 = alma.remap({k: k + '_out' for k in ['a', 'b', 'c']}) -cali2 = SteadyStateDict({'a_out': 1, 'b_out': 2}, internal={}) -ss2 = alma2.steady_state(cali2) -jac2 = alma2.jacobian(cali2, ['a_out']) - -mymap = Bijection({'a': 'A', 'c': 'C'}) - -cali = SteadyStateDict({'a': 1, 'b': 2}, internal={}) -jac = alma.jacobian(cali, ['a']) - - -'''More serious''' - -# household_remapped = household.remap({'beta': 'beta1', 'Mpc': 'Mpc1'}) -# hh = sj.create_model([household_remapped, income_state_vars, asset_state_vars], name='Household') -# dag = sj.create_model([hh, interest_rates, fiscal_dis, mkt_clearing], name='HANK') -# -# calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 11, -# 'amin': 0.0, 'amax': 1000, 'nA': 500, 'Gamma': 0.0, 'rho_B': 0.9, 'transfer': 0.143} -# -# ss = dag.solve_steady_state(calibration, -# unknowns={'beta1': .95, 'G': 0.2, 'B': 2.0}, -# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc1': 0.25}, -# solver='hybr') - -# td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, -# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) -# -# -# td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, -# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) - - - diff --git a/remap_test.py b/remap_test.py index 0419171..2a69197 100644 --- a/remap_test.py +++ b/remap_test.py @@ -111,13 +111,19 @@ def mpcs(c, a, a_grid, r): return mpc +household.add_hetoutput(mpcs, verbose=False) + + '''Part 3: permanent heterogeneity''' -# remap method takes a dict and returns new copies of blocks -household.add_hetoutput(mpcs, verbose=False) -to_map = ['beta', *household.outputs] -hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('patient household') -hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('impatient household') +# permanent types given by CombinedBlocks with different names +hh_patient = create_model([income_state_vars, household], name='patient_hh') +hh_impatient = create_model([income_state_vars, household], name='impatient_hh') + +# remap variables +to_map = ['beta', *hh_patient.outputs] +hh_patient = hh_patient.remap({k: k + '_patient' for k in to_map}) +hh_impatient = hh_impatient.remap({k: k + '_impatient' for k in to_map}) @simple @@ -134,13 +140,16 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_i blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] ks_model = create_model(blocks, name="Krusell-Smith") -# Steady State +# steady state calibration = {'eis': 1, 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, 'nS': 11, 'nA': 500, 'amax': 1000, 'beta_impatient': 0.98, 'mass_patient': 0.5} + +# this should return ss.internals['patient_hh']['household'] and ss.internals['impatient_hh']['household'] ss = ks_model.solve_steady_state(calibration, solver='brentq', unknowns={'beta_patient': (0.97/1.01, 0.999/1.01), 'Z': 0.5, 'K': 8.6}, targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, helper_blocks=[firm_ss], helper_targets=['Y', 'r']) -td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, - unknowns=['K'], targets=['asset_mkt']) +# .jacobian will have to find the internals, .impulse_linear, .impulse_nonlinear should be able to return internals +# td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, +# unknowns=['K'], targets=['asset_mkt']) From 955b90da3a9befa67c7fcd0befac8671dba1de2b Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 21 Jul 2021 20:09:30 -0500 Subject: [PATCH 190/288] Return nested .internal data in steady_state invocations on DAGs with CombinedBlocks --- src/sequence_jacobian/blocks/combined_block.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 3c355c9..08af6c2 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -66,10 +66,15 @@ def _steady_state(self, calibration, helper_blocks=None, **kwargs): topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) blocks_all = self.blocks + helper_blocks - ss_partial_eq = deepcopy(calibration) + ss_partial_eq_toplevel = deepcopy(calibration) + ss_partial_eq_internal = {} for i in topsorted: - ss_partial_eq.update(eval_block_ss(blocks_all[i], ss_partial_eq, **kwargs)) - return SteadyStateDict(ss_partial_eq) + outputs = eval_block_ss(blocks_all[i], ss_partial_eq_toplevel, **kwargs) + ss_partial_eq_toplevel.update(outputs.toplevel) + if outputs.internal: + ss_partial_eq_internal.update(outputs.internal) + ss_partial_eq_internal = {self.name: ss_partial_eq_internal} if ss_partial_eq_internal else {} + return SteadyStateDict(ss_partial_eq_toplevel, internal=ss_partial_eq_internal) def _impulse_nonlinear(self, ss, exogenous, **kwargs): """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from From 54e67dfd302056aa7a7c9fd50412c0f30e570b0f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 26 Jul 2021 12:09:18 -0400 Subject: [PATCH 191/288] Caught bug in ss driver that sabotaged dissolve. --- src/sequence_jacobian/steady_state/drivers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index 3119bfa..c105270 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -157,7 +157,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them # at the provided set of unknowns if included in dissolve. - valid_input_kwargs = misc.input_kwarg_list(block.steady_state) + valid_input_kwargs = misc.input_kwarg_list(block._steady_state) block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} if block in dissolve and "solver" in valid_input_kwargs: @@ -170,7 +170,7 @@ def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **k f"If the user provides a set of top-level unknowns that subsume block-level unknowns," f" it must be explicitly declared in `dissolve`.") - return block.M @ block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) + return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, From a1b8851fe6ba05ebcfdcb91ae8f10f1d67279997 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 26 Jul 2021 13:34:14 -0400 Subject: [PATCH 192/288] Simple ss test for perma heterogeneity using remap. --- couple_endo.py | 405 ------------------ dcblock_test.py | 181 -------- remap_test.py | 155 ------- single_endo.py | 342 --------------- src/sequence_jacobian/models/krusell_smith.py | 22 +- tests/base/test_steady_state.py | 6 + tests/conftest.py | 27 ++ 7 files changed, 48 insertions(+), 1090 deletions(-) delete mode 100644 couple_endo.py delete mode 100644 dcblock_test.py delete mode 100644 remap_test.py delete mode 100644 single_endo.py diff --git a/couple_endo.py b/couple_endo.py deleted file mode 100644 index 5aee8f4..0000000 --- a/couple_endo.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Couple household model with frictional labor supply.""" - -import numpy as np -from numba import guvectorize, njit - -from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved -from sequence_jacobian.blocks.discont_block import discont -from sequence_jacobian.utilities.misc import choice_prob, logsum - - -'''Core HA block''' - - -def household_init(a_grid, z_grid_m, b_grid_m, z_grid_f, b_grid_f, - atw, transfer, rpost, eis, vphi_m, chi_m, vphi_f, chi_f): - _, income = labor_income(z_grid_m, b_grid_m, z_grid_f, b_grid_f, atw, transfer, 1, 1, 0, 0, 0, 0, 0, 0) - coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] - c_guess = 0.3 * coh - V, Va = np.empty_like(coh), np.empty_like(coh) - for iw in range(16): - V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi_m, chi_m, vphi_f, chi_f) - V = V / 0.1 - - # get Va by finite difference - Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) - Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) - Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) - - return Va, V - - -@njit(fastmath=True) -def util(c, iw, eis, vphi_m, chi_m, vphi_f, chi_f): - """Utility function.""" - # 1. utility from consumption. - if eis == 1: - u = np.log(c / 2) - else: - u = (c/2) ** (1 - 1/eis) / (1 - 1/eis) - - # 2. disutility from work and search - if iw <= 3: - u = u - vphi_m # E male - if (iw >= 4) & (iw <= 11): - u = u - chi_m # U male - if np.mod(iw, 4) == 0: - u = u - vphi_f # E female - if (np.mod(iw, 4) == 1) | (np.mod(iw, 4) == 2): - u = u - chi_f # U female - return u - - -@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) -def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, lam_grid, - z_all, b_all, z_man, b_man, z_wom, b_wom, eis, beta, rpost, vphi_m, chi_m, vphi_f, chi_f): - """ - Backward step function EGM with upper envelope. - - Dimensions: 0: labor market status, 1: productivity, 2: assets. - Status: 0: EE, 1: EUb, 2: EU, 3: EN, 4: UbE, 5: UbUb, 6: UbU, 7: UbN, 8: UE, 9: UUb, 10: UU, 11: UN, - 12: NE, 13: NUb, 14: NU, 15: NN - State: 0: MM, 1: MB, 2: ML, 3: BM, 4: BB, 5: BL, 6: LM, 7: LB, 8: LL - - Parameters - ---------- - V_p : array(Ns, Nz, Na), status-specific value function tomorrow - Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow - Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks - Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity - choice_set : list(Nz), discrete choices available in each state X - a_grid : array(Na), exogenous asset grid - y_grid : array(Ns, Nz), exogenous labor income grid - lam_grid : array(Nx), scale of taste shocks, specific to interim state - eis : float, EIS - beta : float, discount factor - rpost : float, ex-post interest rate - - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function today - Va : array(Ns, Nz, Na), partial of status-specific value function today - P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x - c : array(Ns, Nz, Na), status-specific consumption policy today - a : array(Ns, Nz, Na), status-specific asset policy today - ze : array(Ns, Nz, Na), effective labor (average productivity if employed) - ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) - """ - # shapes - Ns, Nz, Na = V_p.shape - Nx = Pi_s_p.shape[1] - - # PART 1: update value and policy functions - # a. discrete choice I expect to make tomorrow - V_p_X = np.empty((Nx, Nz, Na)) - Va_p_X = np.empty((Nx, Nz, Na)) - for ix in range(Nx): - V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) - Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) - P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) - V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) - Va_p_X[ix, ...] = np.sum(P_p_ix*Va_p_ix, axis=0) - - # b. compute expectation wrt labor market shock - V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) - Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) - - # b. compute expectation wrt productivity - V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) - Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) - - # d. consumption today on tomorrow's grid and endogenous asset grid today - W = beta * V_p2 - uc_nextgrid = beta * Va_p2 - c_nextgrid = 2 ** (1 - eis) * uc_nextgrid ** (-eis) - a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) - - # e. upper envelope - imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region - V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi_m, chi_m, - vphi_f, chi_f) - - # f. update Va - uc = 2 ** (1 / eis - 1) * c ** (-1 / eis) - Va = (1 + rpost) * uc - - # PART 2: things we need for GE - # 2/a. asset policy - a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c - - # 2/b. choice probabilities (don't need jacobian) - P = np.zeros((Nx, Ns, Nz, Na)) - for ix in range(Nx): - V_ix = np.take(V, indices=choice_set[ix], axis=0) - P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) - - # 2/c. average productivity of employed - ze = np.zeros_like(a) - ze[0, ...] = z_all[:, np.newaxis] - ze[1, ...], ze[2, ...], ze[3, ...] = z_man[:, np.newaxis], z_man[:, np.newaxis], z_man[:, np.newaxis] - ze[4, ...], ze[8, ...], ze[12, ...] = z_wom[:, np.newaxis], z_wom[:, np.newaxis], z_wom[:, np.newaxis] - - # 2/d. UI claims - ui = np.zeros_like(a) - ui[5, ...] = b_all[:, np.newaxis] - ui[4, ...], ui[6, ...], ui[7, ...] = b_man[:, np.newaxis], b_man[:, np.newaxis], b_man[:, np.newaxis] - ui[1, ...], ui[9, ...], ui[13, ...] = b_wom[:, np.newaxis], b_wom[:, np.newaxis], b_wom[:, np.newaxis] - - return V, Va, a, c, P, ze, ui - - -def labor_income(z_grid_m, b_grid_m, z_grid_f, b_grid_f, atw, transfer, - fU_m, fN_m, s_m, fU_f, fN_f, s_f, rho, expiry): - # 1. income - yE_m, yE_f = atw * z_grid_m + transfer, atw * z_grid_f - yU_m, yU_f = atw * b_grid_m + transfer, atw * b_grid_f - yN_m, yN_f = np.zeros_like(yE_m) + transfer, np.zeros_like(yE_f) - y_EE = (yE_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() - y_EU = (yE_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() - y_EN = (yE_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() - y_UE = (yU_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() - y_UU = (yU_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() - y_UN = (yU_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() - y_NE = (yN_m[:, np.newaxis] + yE_f[np.newaxis, :]).ravel() - y_NU = (yN_m[:, np.newaxis] + yU_f[np.newaxis, :]).ravel() - y_NN = (yN_m[:, np.newaxis] + yN_f[np.newaxis, :]).ravel() - y_grid = np.vstack((y_EE, y_EU, y_EN, y_EN, y_UE, y_UU, y_UN, y_UN, y_NE, y_NU, y_NN, y_NN, y_NE, y_NU, y_NN, y_NN)) - - # 2. transition matrix for joint labor market status - cov = rho * np.sqrt(s_m * s_f * (1 - s_m) * (1 - s_f)) - Pi_s_m = np.array([[1 - s_m, s_m, 0], [fU_m, (1 - fU_m) * (1 - expiry), (1 - fU_m) * expiry], - [fU_m, 0, 1 - fU_m], [fN_m, 0, 1 - fN_m]]) - Pi_s_f = np.array([[1 - s_f, s_f, 0], [fU_f, (1 - fU_f) * (1 - expiry), (1 - fU_f) * expiry], - [fU_f, 0, 1 - fU_f], [fN_f, 0, 1 - fN_f]]) - Pi_s = np.kron(Pi_s_m, Pi_s_f) - - # adjust for correlated job loss - Pi_s[0, 0] += cov # neither loses their job - Pi_s[0, 4] += cov # both lose their job - Pi_s[0, 1] -= cov # only female loses her job - Pi_s[0, 3] -= cov # only male loses his job - - return Pi_s, y_grid - - -"""Supporting functions for HA block""" - - -@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) -def nonconcave(uc_nextgrid, imin, imax): - """Obtain bounds for non-concave region.""" - nA = uc_nextgrid.shape[-1] - vmin = np.inf - vmax = -np.inf - # step 1: find vmin & vmax - for ia in range(nA - 1): - if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: - vmin_temp = uc_nextgrid[ia] - vmax_temp = uc_nextgrid[ia + 1] - if vmin_temp < vmin: - vmin = vmin_temp - if vmax_temp > vmax: - vmax = vmax_temp - - # 2/a Find imin (upper bound) - if vmin == np.inf: - imin_ = 0 - else: - ia = 0 - while ia < nA: - if uc_nextgrid[ia] < vmin: - break - ia += 1 - imin_ = ia - - # 2/b Find imax (lower bound) - if vmax == -np.inf: - imax_ = nA - else: - ia = nA - while ia > 0: - if uc_nextgrid[ia] > vmax: - break - ia -= 1 - imax_ = ia - - imin[:] = imin_ - imax[:] = imax_ - - -@njit -def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): - """ - Interpolate consumption and value function to exogenous grid. Brute force but safe. - Parameters - ---------- - W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) - a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) - c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) - a_grid : array(Na), exogenous asset grid (tomorrow's grid) - y_grid : array(Ns, Nz), labor income - rpost : float, ex-post interest rate - args : (eis, vphi, chi) arguments for utility function - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function on exogenous grid - c : array(Ns, Nz, Na), consumption on exogenous grid - """ - - # 0. initialize - Ns, Nz, Na = W.shape - c = np.zeros_like(W) - V = -np.inf * np.ones_like(W) - - # outer loop could run in parallel - for iw in range(Ns): - for iz in range(Nz): - ycur = y_grid[iw, iz] - imaxcur = imax[iw, iz] - imincur = imin[iw, iz] - - # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. - # in concave region: exploit monotonicity and don't look for extra solutions - for ia in range(Na): - acur = a_grid[ia] - - # Region 1: below non-concave: exploit monotonicity - if (ia <= imaxcur) | (ia >= imincur): - iap = 0 - ap_low = a_nextgrid[iw, iz, iap] - ap_high = a_nextgrid[iw, iz, iap + 1] - while iap < Na - 2: # can this go up all the way? - if ap_high >= acur: - break - iap += 1 - ap_low = ap_high - ap_high = a_nextgrid[iw, iz, iap + 1] - # found bracket, interpolate value function and consumption - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess - c[iw, iz, ia] = c_guess - - # Region 2: non-concave region - else: - # try out all segments of endogenous grid - for iap in range(Na - 1): - # does this endogenous segment bracket ia? - ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] - interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) - - # does it need to be extrapolated above the endogenous grid? - # if needed to be extrapolated below, we would be in constrained case - extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) - - if interp or extrap_above: - # interpolation slopes - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - - # implied guess - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - - # value - v_guess = util(c_guess, iw, *args) + w_guess - - # select best value for this segment - if v_guess > V[iw, iz, ia]: - V[iw, iz, ia] = v_guess - c[iw, iz, ia] = c_guess - - # 2. constrained case: remember that we have the inverse asset policy a(a') - ia = 0 - while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: - c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur - V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] - ia += 1 - - return V, c - - -'''Simple blocks''' - - -def zgrids_couple(zm, zf): - """Combine individual z_grid and b_grid as needed for couple level.""" - zeroes = np.zeros_like(zm) - - # all combinations - z_all = (zm[:, np.newaxis] + zf[np.newaxis, :]).ravel() - z_men = (zm[:, np.newaxis] + zeroes[np.newaxis, :]).ravel() - z_wom = (zeroes[:, np.newaxis] + zf[np.newaxis, :]).ravel() - return z_all, z_men, z_wom - - -@simple -def income_state_vars(mean_m, rho_m, sd_m, mean_f, rho_f, sd_f, nZ, uirate, uicap): - # income husband - z_grid_m, z_pdf_m, z_markov_m = markov_rouwenhorst(rho=rho_m, sigma=sd_m, N=nZ) - z_grid_m *= mean_m - b_grid_m = uirate * z_grid_m - b_grid_m[b_grid_m > uicap] = uicap - - # income wife - z_grid_f, z_pdf_f, z_markov_f = markov_rouwenhorst(rho=rho_f, sigma=sd_f, N=nZ) - z_grid_f *= mean_f - b_grid_f = uirate * z_grid_f - b_grid_f[b_grid_f > uicap] = uicap - - # household income - z_all, z_man, z_wom = zgrids_couple(z_grid_m, z_grid_f) - b_all, b_man, b_wom = zgrids_couple(b_grid_m, b_grid_f) - Pi_z = np.kron(z_markov_m, z_markov_f) - - return z_grid_m, z_grid_f, b_grid_m, b_grid_f, z_all, z_man, z_wom, b_all, b_man, b_wom, Pi_z - - -@simple -def employment_state_vars(lamM, lamB, lamL): - choice_set = [[0, 3, 12, 15], [1, 3, 13, 15], [2, 3, 14, 15], [4, 7, 12, 15], [5, 7, 13, 15], - [6, 7, 14, 15], [8, 11, 12, 15], [9, 11, 13, 15], [10, 11, 14, 15]] - lam_grid = np.array([np.sqrt(2*lamM**2), np.sqrt(lamM**2 + lamB**2), np.sqrt(lamM**2 + lamL**2), - np.sqrt(lamB**2 + lamM**2), np.sqrt(2*lamB**2), np.sqrt(lamB**2 + lamL**2), - np.sqrt(lamL**2 + lamM**2), np.sqrt(lamL**2 + lamB**2), np.sqrt(2*lamL**2)]) - return choice_set, lam_grid - - -@simple -def asset_state_vars(amin, amax, nA): - a_grid = agrid(amin=amin, amax=amax, n=nA) - return a_grid - - -@solved(unknowns={'fU_m': 0.25, 'fN_m': 0.1, 's_m': 0.025}, - targets=['fU_m_res', 'fN_m_res', 's_m_res'], - solver='broyden_custom') -def flows_m(Y, fU_m, fN_m, s_m, fU_eps_m, fN_eps_m, s_eps_m): - fU_m_res = fU_m.ss * (Y / Y.ss) ** fU_eps_m - fU_m - fN_m_res = fN_m.ss * (Y / Y.ss) ** fN_eps_m - fN_m - s_m_res = s_m.ss * (Y / Y.ss) ** s_eps_m - s_m - return fU_m_res, fN_m_res, s_m_res - - -@solved(unknowns={'fU_f': 0.25, 'fN_f': 0.1, 's_f': 0.025}, - targets=['fU_f_res', 'fN_f_res', 's_f_res'], - solver='broyden_custom') -def flows_f(Y, fU_f, fN_f, s_f, fU_eps_f, fN_eps_f, s_eps_f): - fU_f_res = fU_f.ss * (Y / Y.ss) ** fU_eps_f - fU_f - fN_f_res = fN_f.ss * (Y / Y.ss) ** fN_eps_f - fN_f - s_f_res = s_f.ss * (Y / Y.ss) ** s_eps_f - s_f - return fU_f_res, fN_f_res, s_f_res - - -'''Put it together''' - -household.add_hetinput(labor_income, verbose=False) -hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows_m, flows_f, household], - name='CoupleHH') -blocks = [income_state_vars, employment_state_vars, asset_state_vars, flows_m, flows_f, household] diff --git a/dcblock_test.py b/dcblock_test.py deleted file mode 100644 index cf7df30..0000000 --- a/dcblock_test.py +++ /dev/null @@ -1,181 +0,0 @@ -"""This model is to test DisContBlock""" - -import numpy as np - -from sequence_jacobian import simple, create_model, solved -import single_endo as single -import couple_endo as couple - - -'''Rest of the model''' - - -@simple -def firm(Z, K, L, mc, tax, alpha, delta0, delta1, psi): - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - w = (1 - alpha) * mc * Y / L - u = (alpha / delta0 / delta1 * mc * Y / K(-1)) ** (1 / delta1) - delta = delta0 * u ** delta1 - Q = 1 + psi * (K / K(-1) - 1) - I = K - (1 - delta) * K(-1) + psi / 2 * (K / K(-1) - 1) ** 2 * K(-1) - transfer = Y - w * L - I - atw = (1 - tax) * w - return Y, w, u, delta, Q, I, transfer, atw - - -@simple -def monetary(pi, rstar, phi_pi): - # rpost = (1 + rstar(-1) + phi_pi * pi(-1)) / (1 + pi) - 1 - rpost = rstar - return rpost - - -@simple -def nkpc(pi, mc, eps, Y, rpost, kappa): - nkpc_res = kappa * (mc - (eps - 1) / eps) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + rpost(+1)) \ - - (1 + pi).apply(np.log) - return nkpc_res - - -@simple -def valuation(rpost, mc, Y, K, Q, delta, psi, alpha): - val = alpha * mc(+1) * Y(+1) / K - (K(+1) / K - (1 - delta) + psi / 2 * (K(+1) / K - 1) ** 2) + \ - K(+1) / K * Q(+1) - (1 + rpost(+1)) * Q - return val - - -@solved(unknowns={'B': [0.0, 10.0]}, targets=['budget'], solver='brentq') -def fiscal(B, tax, w, rpost, G, Ze, Ui): - budget = (1 + rpost) * B + G + (1 - tax) * w * Ui - tax * w * Ze - B - # tax_rule = tax - tax.ss - phi * (B(-1) - B.ss) / Y.ss - tax_rule = B - B.ss - return budget, tax_rule - - -@simple -def mkt_clearing(A, B, Y, C, I, G, L, Ze): - asset_mkt = A - B - goods_mkt = Y - C - I - G - labor_mkt = L - Ze - return asset_mkt, goods_mkt, labor_mkt - - -@simple -def dividends(transfer, pop_sm, pop_sw, pop_mc, illiq_sm, illiq_sw, illiq_mc): - transfer_sm = illiq_sm * transfer / \ - (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) - transfer_sw = illiq_sw * transfer / \ - (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) - transfer_mc = illiq_mc * transfer / \ - (pop_sm * illiq_sm + pop_sw * illiq_sw + pop_mc * illiq_mc) - return transfer_sm, transfer_sw, transfer_mc - - -@simple -def aggregate(A_sm, C_sm, Ze_sm, Ui_sm, pop_sm, A_sw, C_sw, Ze_sw, Ui_sw, pop_sw, A_mc, C_mc, Ze_mc, Ui_mc, pop_mc): - A = pop_sm * A_sm + pop_sw * A_sw + pop_mc * A_mc - C = pop_sm * C_sm + pop_sw * C_sw + pop_mc * C_mc - Ze = pop_sm * Ze_sm + pop_sw * Ze_sw + pop_mc * Ze_mc - Ui = pop_sm * Ui_sm + pop_sw * Ui_sw + pop_mc * Ui_mc - return A, C, Ze, Ui - - -'''Steady-state helpers''' - - -@simple -def firm_ss(eps, rpost, Y, L, alpha, delta0): - # uses u=1 - mc = (eps - 1) / eps - K = mc * Y * alpha / (rpost + delta0) - delta1 = alpha / delta0 * mc * Y / K - Z = Y / (K ** alpha * L ** (1 - alpha)) - w = (1 - alpha) * mc * Y / L - return mc, K, delta1, Z, w - - -@simple -def fiscal_ss(A, tax, w, Ze, rpost, Ui): - # after hh block - B = A - G = tax * w * Ze - rpost * B - (1 - tax) * w * Ui - return B, G - - -'''Calibration''' - -cali_sm = {'beta': 0.9833, 'vphi': 0.7625, 'chi': 0.5585, 'fU': 0.2499, 'fN': 0.1148, 's': 0.0203, - 'mean_z': 1.0, 'rho_z': 0.98, 'sd_z': 0.943*0.82, - 'fU_eps': 10.69, 'fN_eps': 5.57, 's_eps': -11.17, - 'illiq': 0.25, 'pop': 0.29} - -cali_sw = {'beta': 0.9830, 'vphi': 0.9940, 'chi': 0.4958, 'fU': 0.2207, 'fN': 0.1098, 's': 0.0132, - 'mean_z': 0.8, 'rho_z': 0.98, 'sd_z': 0.86*0.82, - 'fU_eps': 8.72, 'fN_eps': 3.55, 's_eps': -6.55, - 'illiq': 0.15, 'pop': 0.29} - -cali_mc = {'beta': 0.9882, 'rho': 0.042, - 'vphi_m': 0.1545, 'chi_m': 0.2119, 'fU_m': 0.3013, 'fN_m': 0.1840, 's_m': 0.0107, - 'vphi_f': 0.2605, 'chi_f': 0.2477, 'fU_f': 0.2519, 'fN_f': 0.1691, 's_f': 0.0103, - 'mean_m': 1.0, 'rho_m': 0.98, 'sd_m': 0.943*0.82, - 'mean_f': 0.8, 'rho_f': 0.98, 'sd_f': 0.86*0.82, - 'fU_eps_m': 10.37, 'fN_eps_m': 9.74, 's_eps_m': -13.60, - 'fU_eps_f': 8.40, 'fN_eps_f': 2.30, 's_eps_f': -7.87, - 'illiq': 0.6, 'pop': 0.42} - -'''Remap''' - -# variables to remap -to_map_single = ['beta', 'vphi', 'chi', 'fU', 'fN', 's', 'mean_z', 'rho_z', 'sd_z', 'transfer', - 'fU_eps', 'fN_eps', 's_eps', *single.hh.outputs] - -to_map_couple = ['beta', 'transfer', 'rho', - 'vphi_m', 'chi_m', 'fU_m', 'fN_m', 's_m', 'mean_m', 'rho_m', 'sd_m', 'fU_eps_m', 'fN_eps_m', 's_eps_m', - 'vphi_f', 'chi_f', 'fU_f', 'fN_f', 's_f', 'mean_f', 'rho_f', 'sd_f', 'fU_eps_f', 'fN_eps_f', 's_eps_f', - *couple.hh.outputs] - -# remap blocks one-by-one (replicates combinedblock) -blocks_sm = [b.remap({k: k + '_sm' for k in to_map_single} - ).rename(b.name + '_sm') for b in single.blocks] -blocks_sw = [b.remap({k: k + '_sw' for k in to_map_single} - ).rename(b.name + '_sw') for b in single.blocks] -blocks_mc = [b.remap({k: k + '_mc' for k in to_map_couple} - ).rename(b.name + '_mc') for b in couple.blocks] - - -'''Solve ss''' - - -hank = create_model(blocks_sm + blocks_sw + blocks_mc + - [aggregate, dividends, firm, monetary, valuation, nkpc, fiscal, mkt_clearing], name='HANK') - -# remap calibration -cali_sm = {k + '_sm': v for k, v in cali_sm.items()} -cali_sw = {k + '_sw': v for k, v in cali_sw.items()} -cali_mc = {k + '_mc': v for k, v in cali_mc.items()} - -calibration = {**cali_sm, **cali_sw, **cali_mc, - 'eis': 1.0, 'uicap': 0.66, 'uirate': 0.5, 'expiry': 1/6, 'eps': 10.0, 'tax': 0.3, - 'amin': 0.0, 'amax': 500, 'nA': 100, 'lamM': 0.01, 'lamB': 0.01, 'lamL': 0.04, 'nZ': 7, - 'kappa': 0.03, 'phi_pi': 1.25, 'rstar': 0.002, 'pi': 0.0, 'alpha': 0.2, 'psi': 30, 'delta0': 0.0083} - -ss = hank.solve_steady_state(calibration, solver='toms748', dissolve=[fiscal], ttol=1E-6, - unknowns={'L': (1.06, 1.12), - 'Z': 0.63, 'mc': 0.9, 'G': 0.2, 'delta1': 1.2, 'B': 3.0, 'K': 17.0}, - targets={'labor_mkt': 0.0, - 'Y': 1.0, 'nkpc_res': 0.0, 'budget': 0.0, 'asset_mkt': 0.0, 'val': 0.0, 'u': 1.0}, - helper_blocks=[firm_ss, fiscal_ss], - helper_targets=['asset_mkt', 'budget', 'val', 'nkpc_res', 'Y', 'u']) - - -# # jacobians -# J = dict() -# J['household_sm'] = blocks_sm[4].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', 'fU', 'fN', 's'], T=500) -# J['household_sw'] = blocks_sw[4].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', 'fU', 'fN', 's'], T=500) -# J['household_mc'] = blocks_mc[5].jacobian(ss, exogenous=['transfer', 'atw', 'rpost', 'beta', -# 'fU_m', 'fN_m', 's_m', 'fU_f', 'fN_f', 's_f'], T=500) -# -# td_lin = hank.solve_impulse_linear(ss, {'rstar': 0.001*0.9**np.arange(500)}, -# unknowns=['K', 'L', 'mc', 'pi', 'tax'], -# targets=['val', 'goods_mkt', 'labor_mkt', 'nkpc_res', 'tax_rule'], -# Js=J) diff --git a/remap_test.py b/remap_test.py deleted file mode 100644 index 2a69197..0000000 --- a/remap_test.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Simple model to test remapping & multiple hetblocks.""" - -import numpy as np -import copy - -from sequence_jacobian import utilities as utils -from sequence_jacobian import create_model, hetoutput, het, simple - - -'''Part 1: HA block''' - - -def household_init(a_grid, e_grid, r, w, eis): - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - return Va - - -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, e_grid, beta, r, w, eis): - """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. - - Parameters - ---------- - Va_p : array (S, A), marginal value of assets tomorrow - Pi_p : array (S, S), Markov matrix for skills tomorrow - a_grid : array (A), asset grid - e_grid : array (S*B), skill grid - beta : scalar, discount rate today - r : scalar, ex-post real interest rate - w : scalar, wage - eis : scalar, elasticity of intertemporal substitution - - Returns - ---------- - Va : array (S*B, A), marginal value of assets today - a : array (S*B, A), asset policy today - c : array (S*B, A), consumption policy today - """ - uc_nextgrid = (beta * Pi_p) @ Va_p - c_nextgrid = uc_nextgrid ** (-eis) - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) - utils.optimized_routines.setmin(a, a_grid[0]) - c = coh - a - Va = (1 + r) * c ** (-1 / eis) - return Va, a, c - - -def get_mpcs(c, a, a_grid, rpost): - """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" - mpcs = np.empty_like(c) - post_return = (1 + rpost) * a_grid - - # symmetric differences away from boundaries - mpcs[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (post_return[2:] - post_return[:-2]) - - # asymmetric first differences at boundaries - mpcs[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) - mpcs[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) - - # special case of constrained - mpcs[a == a_grid[0]] = 1 - - return mpcs - - -'''Part 2: Simple Blocks''' - - -@simple -def firm(K, L, Z, alpha, delta): - r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta - w = (1 - alpha) * Z * (K(-1) / L) ** alpha - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - I = K - (1 - delta) * K(-1) - return r, w, Y, I - - -@simple -def mkt_clearing(K, A, Y, C, I): - asset_mkt = A - K - goods_mkt = Y - C - I - return asset_mkt, goods_mkt - - -@simple -def income_state_vars(rho, sigma, nS): - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - return e_grid, Pi - - -@simple -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@simple -def firm_ss(r, Y, L, delta, alpha): - rk = r + delta - w = (1 - alpha) * Y / L - K = alpha * Y / rk - Z = Y / K ** alpha / L ** (1 - alpha) - return w, K, Z - - -@hetoutput() -def mpcs(c, a, a_grid, r): - mpc = get_mpcs(c, a, a_grid, r) - return mpc - - -household.add_hetoutput(mpcs, verbose=False) - - -'''Part 3: permanent heterogeneity''' - -# permanent types given by CombinedBlocks with different names -hh_patient = create_model([income_state_vars, household], name='patient_hh') -hh_impatient = create_model([income_state_vars, household], name='impatient_hh') - -# remap variables -to_map = ['beta', *hh_patient.outputs] -hh_patient = hh_patient.remap({k: k + '_patient' for k in to_map}) -hh_impatient = hh_impatient.remap({k: k + '_impatient' for k in to_map}) - - -@simple -def aggregate(A_patient, A_impatient, C_patient, C_impatient, Mpc_patient, Mpc_impatient, mass_patient): - C = mass_patient * C_patient + (1 - mass_patient) * C_impatient - A = mass_patient * A_patient + (1 - mass_patient) * A_impatient - Mpc = mass_patient * Mpc_patient + (1 - mass_patient) * Mpc_impatient - return C, A, Mpc - - -'''Steady state''' - -# DAG -blocks = [hh_patient, hh_impatient, firm, mkt_clearing, income_state_vars, asset_state_vars, aggregate] -ks_model = create_model(blocks, name="Krusell-Smith") - -# steady state -calibration = {'eis': 1, 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, - 'nS': 11, 'nA': 500, 'amax': 1000, 'beta_impatient': 0.98, 'mass_patient': 0.5} - -# this should return ss.internals['patient_hh']['household'] and ss.internals['impatient_hh']['household'] -ss = ks_model.solve_steady_state(calibration, solver='brentq', - unknowns={'beta_patient': (0.97/1.01, 0.999/1.01), 'Z': 0.5, 'K': 8.6}, - targets={'asset_mkt': 0.0, 'Y': 1.0, 'r': 0.01}, - helper_blocks=[firm_ss], helper_targets=['Y', 'r']) - -# .jacobian will have to find the internals, .impulse_linear, .impulse_nonlinear should be able to return internals -# td_nonlin = ks_model.solve_impulse_nonlinear(ss, {'Z': 0.001*0.9**np.arange(300)}, -# unknowns=['K'], targets=['asset_mkt']) diff --git a/single_endo.py b/single_endo.py deleted file mode 100644 index 13dc9e2..0000000 --- a/single_endo.py +++ /dev/null @@ -1,342 +0,0 @@ -"""Single household model with frictional labor supply.""" - -import numpy as np -from numba import guvectorize, njit - -from sequence_jacobian import simple, agrid, markov_rouwenhorst, create_model, solved -from sequence_jacobian.blocks.discont_block import discont -from sequence_jacobian.utilities.misc import choice_prob, logsum - - -'''Core HA block''' - - -def household_init(a_grid, z_grid, b_grid, atw, transfer, rpost, eis, vphi, chi): - _, income = labor_income(z_grid, b_grid, atw, transfer, 0, 1, 1, 1) - coh = income[:, :, np.newaxis] + (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] - c_guess = 0.3 * coh - V, Va = np.empty_like(coh), np.empty_like(coh) - for iw in range(4): - V[iw, ...] = util(c_guess[iw, ...], iw, eis, vphi, chi) - V = V / 0.1 - - # get Va by finite difference - Va[:, :, 1:-1] = (V[:, :, 2:] - V[:, :, :-2]) / (a_grid[2:] - a_grid[:-2]) - Va[:, :, 0] = (V[:, :, 1] - V[:, :, 0]) / (a_grid[1] - a_grid[0]) - Va[:, :, -1] = (V[:, :, -1] - V[:, :, -2]) / (a_grid[-1] - a_grid[-2]) - - return Va, V - - -@njit(fastmath=True) -def util(c, iw, eis, vphi, chi): - """Utility function.""" - # 1. utility from consumption. - if eis == 1: - u = np.log(c) - else: - u = c ** (1 - 1 / eis) / (1 - 1 / eis) - - # 2. disutility from work and search - if iw == 0: - u = u - vphi # E - elif (iw == 1) | (iw == 2): - u = u - chi # Ub, U - - return u - - -@discont(exogenous=('Pi_s', 'Pi_z'), policy='a', disc_policy='P', backward=('V', 'Va'), backward_init=household_init) -def household(V_p, Va_p, Pi_z_p, Pi_s_p, choice_set, a_grid, y_grid, z_grid, b_grid, lam_grid, eis, beta, rpost, - vphi, chi): - """ - Backward step function EGM with upper envelope. - - Dimensions: 0: labor market status, 1: productivity, 2: assets. - Status: 0: E, 1: Ub, 2: U, 3: O - State: 0: M, 1: B, 2: L - - Parameters - ---------- - V_p : array(Ns, Nz, Na), status-specific value function tomorrow - Va_p : array(Ns, Nz, Na), partial of status-specific value function tomorrow - Pi_s_p : array(Ns, Nx), Markov matrix for labor market shocks - Pi_z_p : array(Nz, Nz), (non-status-specific Markov) matrix for productivity - choice_set : list(Nz), discrete choices available in each state X - a_grid : array(Na), exogenous asset grid - y_grid : array(Ns, Nz), exogenous labor income grid - z_grid : array(Nz), productivity of employed (need for GE) - b_grid : array(Nz), productivity of unemployed (need for GE) - lam_grid : array(Nx), scale of taste shocks, specific to interim state - eis : float, EIS - beta : float, discount factor - rpost : float, ex-post interest rate - vphi : float, disutility of work - chi : float, disutility of search - - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function today - Va : array(Ns, Nz, Na), partial of status-specific value function today - P : array(Nx, Ns, Nz, Na), probability of choosing status s in state x - c : array(Ns, Nz, Na), status-specific consumption policy today - a : array(Ns, Nz, Na), status-specific asset policy today - ze : array(Ns, Nz, Na), effective labor (average productivity if employed) - ui : array(Ns, Nz, Na), UI benefit claims (average productivity if unemployed) - """ - # shapes - Ns, Nz, Na = V_p.shape - Nx = Pi_s_p.shape[1] - - # PART 1: update value and policy functions - # a. discrete choice I expect to make tomorrow - V_p_X = np.empty((Nx, Nz, Na)) - Va_p_X = np.empty((Nx, Nz, Na)) - for ix in range(Nx): - V_p_ix = np.take(V_p, indices=choice_set[ix], axis=0) - Va_p_ix = np.take(Va_p, indices=choice_set[ix], axis=0) - P_p_ix = choice_prob(V_p_ix, lam_grid[ix]) - V_p_X[ix, ...] = logsum(V_p_ix, lam_grid[ix]) - Va_p_X[ix, ...] = np.sum(P_p_ix * Va_p_ix, axis=0) - - # b. compute expectation wrt labor market shock - V_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, V_p_X) - Va_p1 = np.einsum('ij,jkl->ikl', Pi_s_p, Va_p_X) - - # b. compute expectation wrt productivity - V_p2 = np.einsum('ij,kjl->kil', Pi_z_p, V_p1) - Va_p2 = np.einsum('ij,kjl->kil', Pi_z_p, Va_p1) - - # d. consumption today on tomorrow's grid and endogenous asset grid today - W = beta * V_p2 - uc_nextgrid = beta * Va_p2 - c_nextgrid = uc_nextgrid ** (-eis) - a_nextgrid = (c_nextgrid + a_grid[np.newaxis, np.newaxis, :] - y_grid[:, :, np.newaxis]) / (1 + rpost) - - # e. upper envelope - imin, imax = nonconcave(uc_nextgrid) # bounds of non-concave region - V, c = upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, eis, vphi, chi) - - # f. update Va - uc = c ** (-1 / eis) - Va = (1 + rpost) * uc - - # PART 2: things we need for GE - - # 2/a. asset policy - a = (1 + rpost) * a_grid[np.newaxis, np.newaxis, :] + y_grid[:, :, np.newaxis] - c - - # 2/b. choice probabilities (don't need jacobian) - P = np.zeros((Nx, Ns, Nz, Na)) - for ix in range(Nx): - V_ix = np.take(V, indices=choice_set[ix], axis=0) - P[ix, choice_set[ix], ...] = choice_prob(V_ix, lam_grid[ix]) - - # 2/c. average productivity of employed - ze = np.zeros_like(a) - ze[0, ...] = z_grid[:, np.newaxis] - - # 2/d. UI claims - ui = np.zeros_like(a) - ui[1, ...] = b_grid[:, np.newaxis] - - return V, Va, a, c, P, ze, ui - - -def labor_income(z_grid, b_grid, atw, transfer, expiry, fU, fN, s): - # 1. income - yE = atw * z_grid + transfer - yUb = atw * b_grid + transfer - yN = np.zeros_like(yE) + transfer - y_grid = np.vstack((yE, yUb, yN, yN)) - - # 2. transition matrix for labor market status - Pi_s = np.array([[1 - s, s, 0], [fU, (1 - fU) * (1 - expiry), (1 - fU) * expiry], [fU, 0, 1 - fU], [fN, 0, 1 - fN]]) - - return Pi_s, y_grid - - -"""Supporting functions for HA block""" - - -@guvectorize(['void(float64[:], uint32[:], uint32[:])'], '(nA) -> (),()', nopython=True) -def nonconcave(uc_nextgrid, imin, imax): - """Obtain bounds for non-concave region.""" - nA = uc_nextgrid.shape[-1] - vmin = np.inf - vmax = -np.inf - # step 1: find vmin & vmax - for ia in range(nA - 1): - if uc_nextgrid[ia + 1] > uc_nextgrid[ia]: - vmin_temp = uc_nextgrid[ia] - vmax_temp = uc_nextgrid[ia + 1] - if vmin_temp < vmin: - vmin = vmin_temp - if vmax_temp > vmax: - vmax = vmax_temp - - # 2/a Find imin (upper bound) - if vmin == np.inf: - imin_ = 0 - else: - ia = 0 - while ia < nA: - if uc_nextgrid[ia] < vmin: - break - ia += 1 - imin_ = ia - - # 2/b Find imax (lower bound) - if vmax == -np.inf: - imax_ = nA - else: - ia = nA - while ia > 0: - if uc_nextgrid[ia] > vmax: - break - ia -= 1 - imax_ = ia - - imin[:] = imin_ - imax[:] = imax_ - - -@njit -def upper_envelope(imin, imax, W, a_nextgrid, c_nextgrid, a_grid, y_grid, rpost, *args): - """ - Interpolate consumption and value function to exogenous grid. Brute force but safe. - Parameters - ---------- - W : array(Ns, Nz, Na), status-specific end-of-period value function (on tomorrow's grid) - a_nextgrid : array(Ns, Nz, Na), endogenous asset grid (today's grid) - c_nextgrid : array(Ns, Nz, Na), consumption on endogenous grid (today's grid) - a_grid : array(Na), exogenous asset grid (tomorrow's grid) - y_grid : array(Ns, Nz), labor income - rpost : float, ex-post interest rate - args : (eis, vphi, chi) arguments for utility function - Returns - ------- - V : array(Ns, Nz, Na), status-specific value function on exogenous grid - c : array(Ns, Nz, Na), consumption on exogenous grid - """ - - # 0. initialize - Ns, Nz, Na = W.shape - c = np.zeros_like(W) - V = -np.inf * np.ones_like(W) - - # outer loop could run in parallel - for iw in range(Ns): - for iz in range(Nz): - ycur = y_grid[iw, iz] - imaxcur = imax[iw, iz] - imincur = imin[iw, iz] - - # 1. unconstrained case: loop through a_grid, find bracketing endogenous gridpoints and interpolate. - # in concave region: exploit monotonicity and don't look for extra solutions - for ia in range(Na): - acur = a_grid[ia] - - # Region 1: below non-concave: exploit monotonicity - if (ia <= imaxcur) | (ia >= imincur): - iap = 0 - ap_low = a_nextgrid[iw, iz, iap] - ap_high = a_nextgrid[iw, iz, iap + 1] - while iap < Na - 2: # can this go up all the way? - if ap_high >= acur: - break - iap += 1 - ap_low = ap_high - ap_high = a_nextgrid[iw, iz, iap + 1] - # found bracket, interpolate value function and consumption - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - V[iw, iz, ia] = util(c_guess, iw, *args) + w_guess - c[iw, iz, ia] = c_guess - - # Region 2: non-concave region - else: - # try out all segments of endogenous grid - for iap in range(Na - 1): - # does this endogenous segment bracket ia? - ap_low, ap_high = a_nextgrid[iw, iz, iap], a_nextgrid[iw, iz, iap + 1] - interp = (ap_low <= acur <= ap_high) or (ap_low >= acur >= ap_high) - - # does it need to be extrapolated above the endogenous grid? - # if needed to be extrapolated below, we would be in constrained case - extrap_above = (iap == Na - 2) and (acur > a_nextgrid[iw, iz, Na - 1]) - - if interp or extrap_above: - # interpolation slopes - w_low, w_high = W[iw, iz, iap], W[iw, iz, iap + 1] - c_low, c_high = c_nextgrid[iw, iz, iap], c_nextgrid[iw, iz, iap + 1] - w_slope = (w_high - w_low) / (ap_high - ap_low) - c_slope = (c_high - c_low) / (ap_high - ap_low) - - # implied guess - c_guess = c_low + c_slope * (acur - ap_low) - w_guess = w_low + w_slope * (acur - ap_low) - - # value - v_guess = util(c_guess, iw, *args) + w_guess - - # select best value for this segment - if v_guess > V[iw, iz, ia]: - V[iw, iz, ia] = v_guess - c[iw, iz, ia] = c_guess - - # 2. constrained case: remember that we have the inverse asset policy a(a') - ia = 0 - while ia < Na and a_grid[ia] <= a_nextgrid[iw, iz, 0]: - c[iw, iz, ia] = (1 + rpost) * a_grid[ia] + ycur - V[iw, iz, ia] = util(c[iw, iz, ia], iw, *args) + W[iw, iz, 0] - ia += 1 - - return V, c - - -'''Simple blocks''' - - -@simple -def income_state_vars(mean_z, rho_z, sd_z, nZ, uirate, uicap): - # productivity - z_grid, pi_z, Pi_z = markov_rouwenhorst(rho=rho_z, sigma=sd_z, N=nZ) - z_grid *= mean_z - - # unemployment benefits - b_grid = uirate * z_grid - b_grid[b_grid > uicap] = uicap - return z_grid, b_grid, pi_z, Pi_z - - -@simple -def employment_state_vars(lamM, lamB, lamL): - choice_set = [[0, 3], [1, 3], [2, 3]] - lam_grid = np.array([lamM, lamB, lamL]) - return choice_set, lam_grid - - -@simple -def asset_state_vars(amin, amax, nA): - a_grid = agrid(amin=amin, amax=amax, n=nA) - return a_grid - - -@solved(unknowns={'fU': 0.25, 'fN': 0.1, 's': 0.025}, targets=['fU_res', 'fN_res', 's_res'], solver='broyden_custom') -def flows(Y, fU, fN, s, fU_eps, fN_eps, s_eps): - fU_res = fU.ss * (Y / Y.ss) ** fU_eps - fU - fN_res = fN.ss * (Y / Y.ss) ** fN_eps - fN - s_res = s.ss * (Y / Y.ss) ** s_eps - s - return fU_res, fN_res, s_res - - -'''Put it together''' - -household.add_hetinput(labor_income, verbose=False) -hh = create_model([income_state_vars, employment_state_vars, asset_state_vars, flows, household], name='SingleHH') -blocks = [income_state_vars, employment_state_vars, asset_state_vars, flows, household] diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py index 378664f..fddd2cd 100644 --- a/src/sequence_jacobian/models/krusell_smith.py +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -78,14 +78,12 @@ def asset_state_vars(amax, nA): @simple -def firm_steady_state_solution(r, delta, alpha): +def firm_steady_state_solution(r, Y, L, delta, alpha): rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - - return Z, K, Y, w + w = (1 - alpha) * Y / L + K = alpha * Y / rk + Z = Y / K ** alpha / L ** (1 - alpha) + return w, K, Z '''Part 3: Steady state''' @@ -127,3 +125,13 @@ def res(beta_loc): 'rho': rho, 'nS': nS, 'asset_mkt': ss['A'] - K}) return ss + + +'''Part 4: Permanent beta heterogeneity''' + + +@simple +def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): + C = mass_patient * C_patient + (1 - mass_patient) * C_impatient + A = mass_patient * A_patient + (1 - mass_patient) * A_impatient + return C, A diff --git a/tests/base/test_steady_state.py b/tests/base/test_steady_state.py index 0a4c372..2a412e1 100644 --- a/tests/base/test_steady_state.py +++ b/tests/base/test_steady_state.py @@ -35,3 +35,9 @@ def test_two_asset_steady_state(two_asset_hank_dag): assert set(ss.keys()) == set(ss_ref.keys()) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) + + +def test_remap_steady_state(ks_remapped_dag): + _, _, _, _, ss = ks_remapped_dag + assert ss['beta_impatient'] < ss['beta_patient'] + assert ss['A_impatient'] < ss['A_patient'] diff --git a/tests/conftest.py b/tests/conftest.py index ab85a96..274d7d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,3 +98,30 @@ def two_asset_hank_dag(): targets = ["asset_mkt", "fisher", "wnkpc"] return two_asset_model, exogenous, unknowns, targets, ss + + +@pytest.fixture(scope='session') +def ks_remapped_dag(): + # Create 2 versions of the household block using `remap` + to_map = ['beta', *krusell_smith.household.outputs] + hh_patient = krusell_smith.household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') + hh_impatient = krusell_smith.household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') + blocks = [hh_patient, hh_impatient, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars, + krusell_smith.asset_state_vars, krusell_smith.aggregate] + ks_remapped = create_model(blocks, name='Krusell-Smith') + + # Steady State + calibration = {'eis': 1., 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, + 'nS': 3, 'nA': 100, 'amax': 1000, 'beta_impatient': 0.985, 'mass_patient': 0.5} + unknowns_ss = {'beta_patient': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.5, 'K': 8.} + targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} + helper_blocks = [krusell_smith.firm_steady_state_solution] + ss = ks_remapped.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', + helper_blocks=helper_blocks, helper_targets=['Y', 'r']) + + # Transitional Dynamics/Jacobian Calculation + exogenous = ["Z"] + unknowns = ["K"] + targets = ["asset_mkt"] + + return ks_remapped, exogenous, unknowns, targets, ss From 94030e6454f6faceeeb41e19f2bd132d2d0bf99a Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 4 Aug 2021 15:27:02 -0400 Subject: [PATCH 193/288] New .partial_jacobian method for CombinedBlocks; mimics curlyJ_sorted --- .../blocks/combined_block.py | 65 ++++++++++++++----- src/sequence_jacobian/primitives.py | 2 +- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 08af6c2..a8249a2 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -1,6 +1,7 @@ """CombinedBlock class and the combine function to generate it""" from copy import deepcopy +import numpy as np from .support.impulse import ImpulseDict from ..primitives import Block @@ -100,26 +101,56 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(irf_lin_partial_eq) + def partial_jacobians(self, ss, inputs=None, T=None, Js=None): + """Calculate partial Jacobians (i.e. without forward accumulation) wrt `inputs` and outputs of other blocks.""" + if inputs is None: + inputs = self.inputs + + # Add intermediate inputs; remove vector-valued inputs + shocks = set(inputs) | self._required + shocks -= set([k for k, v in ss.items() if np.size(v) > 1]) + + # Compute Jacobians along the DAG + curlyJs = {} + kwargs = {"exogenous": shocks, "T": T, "Js": Js} + for block in self.blocks: + # Don't remap here + curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block._jacobian) + if k in kwargs}) + + # Don't return empty Jacobians + if curlyJ.outputs: + curlyJs[block.name] = curlyJ + + return curlyJs + def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at a steady state, `ss`""" - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None: - outputs = self.outputs - kwargs = {"exogenous": exogenous, "T": T, "outputs": outputs, "Js": Js} - - for i, block in enumerate(self.blocks): - curlyJ = block._jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block._jacobian) if k in kwargs}).complete() - - # If we want specific list of outputs, restrict curlyJ to that before continuing - curlyJ = curlyJ[[k for k in curlyJ.outputs if k in outputs or k in self._required]] - if i == 0: - J_partial_eq = curlyJ.compose(JacobianDict.identity(exogenous)) - else: - J_partial_eq.update(curlyJ.compose(J_partial_eq)) - - return J_partial_eq + if outputs is not None: + # if list of outputs provided, we need to obtain these and 'required' along the way + alloutputs = set(outputs) | self._required + else: + # otherwise, set to None, implies default behavior of obtaining all outputs in curlyJs + alloutputs = None + + # Compute all partial Jacobians + curlyJs = self.partial_jacobians(ss, inputs=exogenous, T=T, Js=Js) + + # Forward accumulate partial Jacobians + out = JacobianDict.identity(exogenous) + for curlyJ in curlyJs.values(): + if alloutputs is not None: + # don't accumulate derivatives we don't need or care about + curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] + out.update(curlyJ.compose(out)) + + if outputs is not None: + # don't return derivatives that we don't care about (even if required them above) + return out[[k for k in outputs if k in out.outputs]] + else: + return out + def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, sort_blocks=False, **kwargs): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 042e1c1..f438d13 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -87,7 +87,7 @@ def impulse_linear(self, ss: SteadyStateDict, return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) def jacobian(self, ss: SteadyStateDict, - exogenous: Dict[str, Array], + exogenous: List[str], T: Optional[int] = None, **kwargs) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`.""" return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, T=T, **kwargs) From 9f8daa1f744eee1020b8f998ef85242df916d755 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 5 Aug 2021 10:02:33 -0400 Subject: [PATCH 194/288] HetBlocks look for `self.exogenous` in internals. --- src/sequence_jacobian/blocks/het_block.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 2af7d55..d36fe5b 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -271,7 +271,7 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa T = shock_lengths[0] # copy from ss info - Pi_T = ss[self.exogenous].T.copy() + Pi_T = ss.internal[self.name][self.exogenous].T.copy() D = ss.internal[self.name]['D'] # construct grids for policy variables either from the steady state grid if the grid is meant to be @@ -736,7 +736,7 @@ def jac_prelim(self, ss): """ # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation ssin_dict = self.make_inputs(ss) - Pi = ss[self.exogenous] + Pi = ss.internal[self.name][self.exogenous] grid = {k: ss[k+'_grid'] for k in self.policy} ssout_list = self.back_step_fun(**ssin_dict) From e6c4bca83b2f46c31c1ca12e4eb97229dbfd1da9 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 10 Aug 2021 10:57:16 -0400 Subject: [PATCH 195/288] test file for nested combinedblocks --- nesting_test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 nesting_test.py diff --git a/nesting_test.py b/nesting_test.py new file mode 100644 index 0000000..e69de29 From ccd9750b921072bb818b6e945f4cd1f89f193ebf Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 10 Aug 2021 10:58:03 -0400 Subject: [PATCH 196/288] test file for nested combinedblocks for real --- nesting_test.py | 183 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/nesting_test.py b/nesting_test.py index e69de29..0ac8a4e 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -0,0 +1,183 @@ +import numpy as np +import sequence_jacobian as sj +from sequence_jacobian import het, simple, hetoutput, combine, create_model, get_H_U +from sequence_jacobian.jacobian.classes import JacobianDict, ZeroMatrix + + +'''Part 1: Household block''' + + +def household_init(a_grid, y, rpost, sigma): + c = np.maximum(1e-8, y[:, np.newaxis] + + np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) + Va = (1 + rpost) * (c ** (-sigma)) + return Va + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, Pi_p, a_grid, y, rpost, beta, sigma): + """ + Backward step in simple incomplete market model. Assumes CRRA utility. + Parameters + ---------- + Va_p : array (E, A), marginal value of assets tomorrow (backward iterator) + Pi_p : array (E, E), Markov matrix for skills tomorrow + a_grid : array (A), asset grid + y : array (E), non-financial income + rpost : scalar, ex-post return on assets + beta : scalar, discount factor + sigma : scalar, utility parameter + Returns + ------- + Va : array (E, A), marginal value of assets today + a : array (E, A), asset policy today + c : array (E, A), consumption policy today + """ + uc_nextgrid = (beta * Pi_p) @ Va_p + c_nextgrid = uc_nextgrid ** (-1 / sigma) + coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] + a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) + sj.utilities.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + uc = c ** (-sigma) + Va = (1 + rpost) * uc + + return Va, a, c + + +def get_mpcs(c, a, a_grid, rpost): + """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" + mpcs_ = np.empty_like(c) + post_return = (1 + rpost) * a_grid + + # symmetric differences away from boundaries + mpcs_[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / \ + (post_return[2:] - post_return[:-2]) + + # asymmetric first differences at boundaries + mpcs_[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) + mpcs_[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) + + # special case of constrained + mpcs_[a == a_grid[0]] = 1 + + return mpcs_ + + +def income(tau, Y, e_grid, e_dist, Gamma, transfer): + """Labor income on the grid.""" + gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, + e_grid ** (1 + Gamma * np.log(Y))) + y = (1 - tau) * Y * gamma * e_grid + transfer + return y + + +@simple +def income_state_vars(rho_e, sd_e, nE): + e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst( + rho=rho_e, sigma=sd_e, N=nE) + return e_grid, e_dist, Pi + + +@simple +def asset_state_vars(amin, amax, nA): + a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +@hetoutput() +def mpcs(c, a, a_grid, rpost): + """MPC out of lump-sum transfer.""" + mpc = get_mpcs(c, a, a_grid, rpost) + return mpc + + +household.add_hetinput(income, verbose=False) +household.add_hetoutput(mpcs, verbose=False) + + +'''Part 2: rest of the model''' + + +@simple +def interest_rates(r): + rpost = r(-1) # household ex-post return + rb = r(-1) # rate on 1-period real bonds + return rpost, rb + + +@simple +def fiscal(B, G, rb, Y, transfer): + rev = rb * B + G + transfer # revenue to be raised + tau = rev / Y + return rev, tau + + +@simple +def mkt_clearing(A, B, C, Y, G): + asset_mkt = A - B + goods_mkt = Y - C - G + return asset_mkt, goods_mkt + + +'''try this''' + +# flat dag +# dag = sj.create_model([household, income_state_vars, asset_state_vars, interest_rates, fiscal, mkt_clearing], +# name='HANK') + + +# nested dag +hh = combine([household, income_state_vars, asset_state_vars], name='HH') +dag = sj.create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') + +calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 11, + 'amin': 0.0, 'amax': 1000, 'nA': 500, 'Gamma': 0.0, 'transfer': 0.143} + +ss = dag.solve_steady_state(calibration, solver='hybr', + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) + + +# Partial Jacobians +# J_ir = interest_rates.jacobian(ss, ['r', 'tau']) +# J_ha = household.jacobian(ss, ['rpost', 'tau'], T=5) +# J1 = J_ha.compose(J_ir) + +# Let's make a simple combined block +# hh = sj.combine([interest_rates, household], name='HH') +# J2 = hh.jacobian(ss, exogenous=['r', 'tau'], T=4) + + +'''Test H_U''' + +unknowns = ['Y'] +targets = ['asset_mkt'] +exogenous = ['r'] +T = 5 + +# J = dag.partial_jacobians(ss, inputs=unknowns + exogenous, T=5) + +# HZ1 = dag.jacobian(ss, exogenous=exogenous, outputs=targets, T=5) +# HZ2 = get_H_U(dag.blocks, exogenous, targets, T, ss=ss) +# np.all(HZ1['asset_mkt']['r'] == HZ2) +# +# HU1 = dag.jacobian(ss, exogenous=unknowns, outputs=targets, T=5) +# HU2 = get_H_U(dag.blocks, unknowns, targets, T, ss=ss) +# np.all(HU1 == HU2) + +# J_int = interest_rates.jacobian(ss, exogenous=['r']) +# J_hh = household.jacobian(ss, exogenous=['Y', 'rpost'], T=4) +# +# # This should have +# J_all1 = J_hh.compose(J_int) +# J_all2 = J_int.compose(J_hh) + + +# G = dag.solve_jacobian(ss, exogenous=['r'], unknowns=['Y'], targets=['asset_mkt'], T=300) + +# td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, +# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) + +# td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, +# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) From edbe246d474d120afe10166efbd3dac9df93c9f4 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 11:23:43 -0500 Subject: [PATCH 197/288] added Parent class to keep track of blocks that are parents of others. seems to work but now some naming errors in two_asset.py, so tests fail --- src/sequence_jacobian/__init__.py | 2 +- .../blocks/combined_block.py | 9 ++- src/sequence_jacobian/blocks/discont_block.py | 2 +- src/sequence_jacobian/blocks/het_block.py | 2 +- src/sequence_jacobian/blocks/parent.py | 60 +++++++++++++++++++ src/sequence_jacobian/blocks/simple_block.py | 2 +- src/sequence_jacobian/blocks/solved_block.py | 9 ++- .../blocks/test_parent_block.py | 17 ++++++ src/sequence_jacobian/primitives.py | 4 +- 9 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 src/sequence_jacobian/blocks/parent.py create mode 100644 src/sequence_jacobian/blocks/test_parent_block.py diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index b09c1f0..5e22830 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -2,7 +2,7 @@ from . import estimation, jacobian, nonlinear, utilities, devtools -from .models import rbc, krusell_smith, hank, two_asset +#from .models import rbc, krusell_smith, hank, two_asset from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index a8249a2..80adbfe 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -7,11 +7,11 @@ from ..primitives import Block from .. import utilities as utils from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock +from ..blocks.parent import Parent from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict from ..steady_state.classes import SteadyStateDict -from .support.bijection import Bijection def combine(blocks, name="", model_alias=False): @@ -23,7 +23,7 @@ def create_model(blocks, **kwargs): return combine(blocks, model_alias=True, **kwargs) -class CombinedBlock(Block): +class CombinedBlock(Block, Parent): """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, and calculates Jacobians along the DAG""" @@ -31,18 +31,21 @@ class CombinedBlock(Block): # CombinedBlock has some automated features that are inferred from initial instantiation but not from # re-assignment of attributes post-instantiation. def __init__(self, blocks, name="", model_alias=False): + super().__init__() self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] self._sorted_indices = utils.graph.block_sort(blocks) self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] - self.M = Bijection({}) if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" else: self.name = name + # now that it has a name, do Parent initialization + Parent.__init__(self, blocks) + # Find all outputs (including those used as intermediary inputs) self.outputs = set().union(*[block.outputs for block in self.blocks]) diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index 091acda..a35d237 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -57,7 +57,7 @@ def __init__(self, back_step_fun, exogenous, policy, disc_policy, backward, back Currently, we only support up to two policy variables. """ self.name = back_step_fun.__name__ - self.M = Bijection({}) + super().__init__() # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index d36fe5b..03e66a6 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -57,7 +57,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non Currently, we only support up to two policy variables. """ self.name = back_step_fun.__name__ - self.M = Bijection({}) + super().__init__() # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) diff --git a/src/sequence_jacobian/blocks/parent.py b/src/sequence_jacobian/blocks/parent.py new file mode 100644 index 0000000..caa961e --- /dev/null +++ b/src/sequence_jacobian/blocks/parent.py @@ -0,0 +1,60 @@ +class Parent: + def __init__(self, blocks, name=None): + # dict from names to immediate kid blocks themselves + # dict from descendants to the names of kid blocks through which to access them + # "descendants" of a block include itself + if not hasattr(self, 'name') and name is not None: + self.name = name + + kids = {} + descendants = {} + + for block in blocks: + kids[block.name] = block + + if isinstance(block, Parent): + for k in block.descendants: + if k in descendants: + raise ValueError(f'Overlapping block name {k}') + descendants[k] = block.name + else: + descendants[block.name] = block.name + + # add yourself to descendants too! but you don't belong to any kid... + if self.name in descendants: + raise ValueError(f'Overlapping block name {self.name}') + descendants[self.name] = None + + self.kids = kids + self.descendants = descendants + + def __getitem__(self, k): + if k == self.name: + return self + elif k in self.kids: + return self.kids[k] + else: + return self.kids[self.descendants[k]][k] + + def select(self, d, kid): + """If d is a dict with block names as keys and kid is a kid, select only the entries in d that are descendants of kid""" + return {k: v for k, v in d.items() if k in self.kids[kid].descendants} + + def path(self, k, reverse=True): + if k not in self.descendants: + raise KeyError(f'Cannot get path to {k} because it is not a descendant of current block') + + if k != self.name: + kid = self.kids[self.descendants[k]] + if isinstance(kid, Parent): + p = kid.path(k, reverse=False) + else: + p = [k] + else: + p = [] + p.append(self.name) + + if reverse: + return list(reversed(p)) + else: + return p diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index ae6ee2b..6fafebc 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -35,13 +35,13 @@ def production(Z, K, L, alpha): """ def __init__(self, f): + super().__init__() self.f = f self.name = f.__name__ self.input_list = misc.input_list(f) self.output_list = misc.output_list(f) self.inputs = set(self.input_list) self.outputs = set(self.output_list) - self.M = Bijection({}) def __repr__(self): return f"" diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index f2e1217..d68c1d0 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -2,8 +2,8 @@ from ..primitives import Block from ..blocks.simple_block import simple +from ..blocks.parent import Parent from ..utilities import graph -from .support.bijection import Bijection from ..jacobian.classes import JacobianDict @@ -26,7 +26,7 @@ def singleton_solved_block(f): return singleton_solved_block -class SolvedBlock(Block): +class SolvedBlock(Block, Parent): """SolvedBlocks are mini SHADE models embedded as blocks inside larger SHADE models. When creating them, we need to provide the basic ingredients of a SHADE model: the list of @@ -42,7 +42,7 @@ class SolvedBlock(Block): def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. self._blocks_unsorted = blocks - self.M = Bijection({}) # don't inherit membrane from parent blocks (think more about this later) + super().__init__() # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort @@ -63,6 +63,9 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ self.solver = solver self.solver_kwargs = solver_kwargs + # initialize as parent + Parent.__init__(self, self.blocks) + # need to have inputs and outputs!!! self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs diff --git a/src/sequence_jacobian/blocks/test_parent_block.py b/src/sequence_jacobian/blocks/test_parent_block.py new file mode 100644 index 0000000..73903c8 --- /dev/null +++ b/src/sequence_jacobian/blocks/test_parent_block.py @@ -0,0 +1,17 @@ +from parent_block import ParentBlock + +class DummyBlock: + def __init__(self, name): + self.name = name + +def test(): + b = ParentBlock([DummyBlock('kid1'), + ParentBlock([DummyBlock('grandkid1'), + DummyBlock('grandkid2')], name='kid2'), + DummyBlock('kid3')], name='me') + + b1 = b['grandkid1'] + assert isinstance(b1, DummyBlock) and b1.name == 'grandkid1' + + print(b.path('grandkid2')) + assert b.path('grandkid2') == ['me', 'kid2', 'grandkid2'] diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index f438d13..b726a0c 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -58,9 +58,9 @@ def __call__(cls, *args, **kwargs): class Block(abc.ABC, metaclass=ABCMeta): """The abstract base class for all `Block` objects.""" - @abc.abstractmethod + #@abc.abstractmethod def __init__(self): - pass + self.M = Bijection({}) @abstract_attribute def inputs(self): From 7bd1f22cfd3fff42234505c113f36fbdbeeeab2e Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 15:46:18 -0500 Subject: [PATCH 198/288] added get_attribute functionality to parent.py that respects remapping and can help us look up .unknowns in deeply nested SolvedBlocks for dissolve, etc --- src/sequence_jacobian/blocks/parent.py | 19 +++++++++++++++ .../blocks/test_parent_block.py | 24 +++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/sequence_jacobian/blocks/parent.py b/src/sequence_jacobian/blocks/parent.py index caa961e..b6d564c 100644 --- a/src/sequence_jacobian/blocks/parent.py +++ b/src/sequence_jacobian/blocks/parent.py @@ -58,3 +58,22 @@ def path(self, k, reverse=True): return list(reversed(p)) else: return p + + def get_attribute(self, k, attr): + """Gets attribute attr from descendant k, respecting any remapping + along the way (requires that attr is list, dict, set)""" + if k == self.name: + inner = getattr(self, attr) + else: + kid = self.kids[self.descendants[k]] + if isinstance(kid, Parent): + inner = kid.get_attribute(k, attr) + else: + inner = getattr(kid, attr) + if hasattr(kid, 'M'): + inner = kid.M @ inner + + if hasattr(self, 'M'): + return self.M @ inner + else: + return inner diff --git a/src/sequence_jacobian/blocks/test_parent_block.py b/src/sequence_jacobian/blocks/test_parent_block.py index 73903c8..3bb06f4 100644 --- a/src/sequence_jacobian/blocks/test_parent_block.py +++ b/src/sequence_jacobian/blocks/test_parent_block.py @@ -1,17 +1,27 @@ -from parent_block import ParentBlock +from parent import Parent +from sequence_jacobian.blocks.support.bijection import Bijection class DummyBlock: def __init__(self, name): self.name = name def test(): - b = ParentBlock([DummyBlock('kid1'), - ParentBlock([DummyBlock('grandkid1'), - DummyBlock('grandkid2')], name='kid2'), - DummyBlock('kid3')], name='me') + grand1 = DummyBlock('grandkid1') + grand2 = DummyBlock('grandkid2') + grand2.unknowns = {'thing1': 3, 'thing2': 5} + grand2.M = Bijection({'thing1': 'othername1'}) + + kid2 = Parent([grand1, grand2], name='kid2') + kid2.M = Bijection({'thing2': 'othername2', 'othername1': 'othername3'}) + + b = Parent([DummyBlock('kid1'), + kid2, + DummyBlock('kid3')], name='me') b1 = b['grandkid1'] assert isinstance(b1, DummyBlock) and b1.name == 'grandkid1' - - print(b.path('grandkid2')) assert b.path('grandkid2') == ['me', 'kid2', 'grandkid2'] + + assert b.get_attribute('grandkid2', 'unknowns') == {'othername3': 3, 'othername2': 5} + +test() From c12ccf21be5eacedcc40d78a3044534215ae28ca Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 16:38:24 -0500 Subject: [PATCH 199/288] internals no longer nested, overhead for dissolve and also selecting input list now in Block.steady_state --- .../blocks/combined_block.py | 4 +- src/sequence_jacobian/blocks/parent.py | 2 + src/sequence_jacobian/blocks/simple_block.py | 1 + src/sequence_jacobian/primitives.py | 18 +++++- src/sequence_jacobian/steady_state/classes.py | 11 ++++ src/sequence_jacobian/steady_state/drivers.py | 62 +++++++++---------- 6 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 80adbfe..62bfce4 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -8,7 +8,6 @@ from .. import utilities as utils from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from ..blocks.parent import Parent -from ..steady_state.drivers import eval_block_ss from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict from ..steady_state.classes import SteadyStateDict @@ -73,11 +72,10 @@ def _steady_state(self, calibration, helper_blocks=None, **kwargs): ss_partial_eq_toplevel = deepcopy(calibration) ss_partial_eq_internal = {} for i in topsorted: - outputs = eval_block_ss(blocks_all[i], ss_partial_eq_toplevel, **kwargs) + outputs = blocks_all[i].steady_state(ss_partial_eq_toplevel, **kwargs) ss_partial_eq_toplevel.update(outputs.toplevel) if outputs.internal: ss_partial_eq_internal.update(outputs.internal) - ss_partial_eq_internal = {self.name: ss_partial_eq_internal} if ss_partial_eq_internal else {} return SteadyStateDict(ss_partial_eq_toplevel, internal=ss_partial_eq_internal) def _impulse_nonlinear(self, ss, exogenous, **kwargs): diff --git a/src/sequence_jacobian/blocks/parent.py b/src/sequence_jacobian/blocks/parent.py index b6d564c..e54d438 100644 --- a/src/sequence_jacobian/blocks/parent.py +++ b/src/sequence_jacobian/blocks/parent.py @@ -1,4 +1,6 @@ class Parent: + # see tests in test_parent_block.py + def __init__(self, blocks, name=None): # dict from names to immediate kid blocks themselves # dict from descendants to the names of kid blocks through which to access them diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 6fafebc..96ab3a8 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -34,6 +34,7 @@ def production(Z, K, L, alpha): Key methods are .ss, .td, and .jac, like HetBlock. """ + # TODO: get rid of .input_list because it serves no function def __init__(self, f): super().__init__() self.f = f diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index b726a0c..fc10118 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -10,10 +10,12 @@ from .steady_state.support import provide_solver_default from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G -from .steady_state.classes import SteadyStateDict +from .steady_state.classes import SteadyStateDict, UserProvidedSS, make_steadystatedict from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict from .blocks.support.bijection import Bijection +from .blocks.parent import Parent +from .utilities import misc # Basic types Array = Any @@ -61,6 +63,7 @@ class Block(abc.ABC, metaclass=ABCMeta): #@abc.abstractmethod def __init__(self): self.M = Bijection({}) + self.ss_valid_input_kwargs = misc.input_kwarg_list(self._steady_state) @abstract_attribute def inputs(self): @@ -70,9 +73,18 @@ def inputs(self): def outputs(self): pass - def steady_state(self, calibration: SteadyStateDict, **kwargs) -> SteadyStateDict: + def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], + dissolve: Optional[List[str]] = [], **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" - return self.M @ self._steady_state(self.M.inv @ calibration, **kwargs) + # special handling: add all unknowns of dissolved blocks to inputs + inputs = self.inputs.copy() + if isinstance(self, Parent): + for k in dissolve: + inputs |= self.get_attribute(k, 'unknowns').keys() + + calibration = make_steadystatedict(calibration)[inputs] + + return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) def impulse_nonlinear(self, ss: SteadyStateDict, exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index c281b74..9f34d7a 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -5,6 +5,9 @@ from ..utilities.misc import dict_diff from ..blocks.support.bijection import Bijection +from numbers import Real +from typing import Any, Dict, Union +Array = Any class SteadyStateDict: def __init__(self, data, internal=None): @@ -77,3 +80,11 @@ def update(self, data, internal_namespaces=None): def difference(self, data_to_remove): return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) + +UserProvidedSS = Dict[str, Union[Real, Array]] + +def make_steadystatedict(ss: Union[SteadyStateDict, UserProvidedSS]) -> SteadyStateDict: + if not isinstance(ss, SteadyStateDict): + return SteadyStateDict(ss) + else: + return ss diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index c105270..3da01aa 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -13,7 +13,7 @@ # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, dissolve=None, +def steady_state(blocks, calibration, unknowns, targets, dissolve=[], sort_blocks=True, helper_blocks=None, helper_targets=None, consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, @@ -101,7 +101,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c for i in topsorted: if not include_helpers and blocks_all[i] in helper_blocks: continue - outputs = eval_block_ss(blocks_all[i], ss_values, hetoutput=True, toplevel_unknowns=unknown_keys, + outputs = blocks_all[i].steady_state(ss_values, hetoutput=True, dissolve=dissolve, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) @@ -142,35 +142,35 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c return ss_values -def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): - """Evaluate the .ss method of a block, given a dictionary of potential arguments""" - if toplevel_unknowns is None: - toplevel_unknowns = [] - if dissolve is None: - dissolve = [] - - # Add the block's internal variables as inputs, if the block has an internal attribute - if isinstance(calibration, dict): - input_arg_dict = deepcopy(calibration) - else: - input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - - # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them - # at the provided set of unknowns if included in dissolve. - valid_input_kwargs = misc.input_kwarg_list(block._steady_state) - block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False - input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} - if block in dissolve and "solver" in valid_input_kwargs: - input_kwarg_dict["solver"] = "solved" - input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} - elif block not in dissolve and block_unknowns_in_toplevel_unknowns: - raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," - f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," - f" {set(toplevel_unknowns)}.\n" - f"If the user provides a set of top-level unknowns that subsume block-level unknowns," - f" it must be explicitly declared in `dissolve`.") - - return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) +# def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): +# """Evaluate the .ss method of a block, given a dictionary of potential arguments""" +# if toplevel_unknowns is None: +# toplevel_unknowns = [] +# if dissolve is None: +# dissolve = [] + +# # Add the block's internal variables as inputs, if the block has an internal attribute +# if isinstance(calibration, dict): +# input_arg_dict = deepcopy(calibration) +# else: +# input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel + +# # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them +# # at the provided set of unknowns if included in dissolve. +# valid_input_kwargs = misc.input_kwarg_list(block._steady_state) +# block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False +# input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} +# if block in dissolve and "solver" in valid_input_kwargs: +# input_kwarg_dict["solver"] = "solved" +# input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} +# elif block not in dissolve and block_unknowns_in_toplevel_unknowns: +# raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," +# f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," +# f" {set(toplevel_unknowns)}.\n" +# f"If the user provides a set of top-level unknowns that subsume block-level unknowns," +# f" it must be explicitly declared in `dissolve`.") + +# return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, From 0b35d5dd56abe527d15deed99e85187058d311d8 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 17:10:54 -0500 Subject: [PATCH 200/288] partially refactored steady_state, new dissolve works in nesting_test.py --- nesting_test.py | 19 +++++++++++++++++-- .../blocks/combined_block.py | 6 ++++-- src/sequence_jacobian/blocks/parent.py | 2 +- src/sequence_jacobian/blocks/solved_block.py | 8 ++++++-- src/sequence_jacobian/primitives.py | 6 ++++++ src/sequence_jacobian/steady_state/drivers.py | 5 ++++- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 0ac8a4e..93b3802 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -1,6 +1,6 @@ import numpy as np import sequence_jacobian as sj -from sequence_jacobian import het, simple, hetoutput, combine, create_model, get_H_U +from sequence_jacobian import het, simple, hetoutput, combine, solved, create_model, get_H_U from sequence_jacobian.jacobian.classes import JacobianDict, ZeroMatrix @@ -134,10 +134,25 @@ def mkt_clearing(A, B, C, Y, G): calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 11, 'amin': 0.0, 'amax': 1000, 'nA': 500, 'Gamma': 0.0, 'transfer': 0.143} -ss = dag.solve_steady_state(calibration, solver='hybr', +ss0 = dag.solve_steady_state(calibration, solver='hybr', unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +def fiscal_solved(B, G, rb, Y, transfer, rho_B): + B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau + +dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +calibration['rho_B'] = 0.8 + +ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) + +assert all(np.allclose(ss0[k], ss[k]) for k in ss0) # Partial Jacobians # J_ir = interest_rates.jacobian(ss, ['r', 'tau']) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 62bfce4..4a291cd 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -61,7 +61,7 @@ def __repr__(self): else: return f"" - def _steady_state(self, calibration, helper_blocks=None, **kwargs): + def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" if helper_blocks is None: helper_blocks = [] @@ -72,7 +72,9 @@ def _steady_state(self, calibration, helper_blocks=None, **kwargs): ss_partial_eq_toplevel = deepcopy(calibration) ss_partial_eq_internal = {} for i in topsorted: - outputs = blocks_all[i].steady_state(ss_partial_eq_toplevel, **kwargs) + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == blocks_all[i].name] + outputs = blocks_all[i].steady_state(ss_partial_eq_toplevel, dissolve=inner_dissolve, **kwargs) ss_partial_eq_toplevel.update(outputs.toplevel) if outputs.internal: ss_partial_eq_internal.update(outputs.internal) diff --git a/src/sequence_jacobian/blocks/parent.py b/src/sequence_jacobian/blocks/parent.py index e54d438..6c3b1ff 100644 --- a/src/sequence_jacobian/blocks/parent.py +++ b/src/sequence_jacobian/blocks/parent.py @@ -1,6 +1,6 @@ class Parent: # see tests in test_parent_block.py - + def __init__(self, blocks, name=None): # dict from names to immediate kid blocks themselves # dict from descendants to the names of kid blocks through which to access them diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index d68c1d0..a297d94 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -22,7 +22,7 @@ def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name else: # call as decorator, return function of function def singleton_solved_block(f): - return SolvedBlock([simple(f)], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + return SolvedBlock([simple(f).rename(f.__name__ + '_inner')], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) return singleton_solved_block @@ -73,8 +73,12 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ def __repr__(self): return f"" - def _steady_state(self, calibration, unknowns=None, helper_blocks=None, solver=None, + def _steady_state(self, calibration, dissolve=[], unknowns=None, helper_blocks=None, solver=None, consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): + if self.name in dissolve: + solver = "solved" + unknowns = {k: v for k, v in calibration.items() if k in unknowns} + # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks if self._sorted_indices_w_helpers is None: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index fc10118..cefef3c 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -83,6 +83,12 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], inputs |= self.get_attribute(k, 'unknowns').keys() calibration = make_steadystatedict(calibration)[inputs] + kwargs['dissolve'] = dissolve + + # TODO: does this respect remapping? + if self.name in dissolve and "solver" in self.ss_valid_input_kwargs: + kwargs["solver"] = "solved" + kwargs["unknowns"] = {k: v for k, v in calibration.items() if k in self.unknowns} return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index 3da01aa..a5c0873 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -10,6 +10,7 @@ subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs from .classes import SteadyStateDict from ..utilities import solvers, graph, misc +from ..blocks.parent import Parent # Find the steady state solution @@ -101,8 +102,10 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c for i in topsorted: if not include_helpers and blocks_all[i] in helper_blocks: continue + # TODO: this is duplicate of CombinedBlock inner_dissolve, should offload to that + inner_dissolve = [k for k in dissolve if isinstance(blocks_all[i], Parent) and k in blocks_all[i].descendants] outputs = blocks_all[i].steady_state(ss_values, hetoutput=True, - dissolve=dissolve, verbose=verbose, **block_kwargs) + dissolve=inner_dissolve, verbose=verbose, **block_kwargs) if include_helpers and blocks_all[i] in helper_blocks: helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) ss_values.update(outputs) From 5098333e977448a9909151a3e51e0d7a53d2f8db Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 17:21:21 -0500 Subject: [PATCH 201/288] minor changes so pytest can run, one error thrown because JacobianDictBlock does not have a name? --- src/sequence_jacobian/__init__.py | 2 +- .../blocks/{test_parent_block.py => adhoc_test_parent_block.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/sequence_jacobian/blocks/{test_parent_block.py => adhoc_test_parent_block.py} (100%) diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index 5e22830..b09c1f0 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -2,7 +2,7 @@ from . import estimation, jacobian, nonlinear, utilities, devtools -#from .models import rbc, krusell_smith, hank, two_asset +from .models import rbc, krusell_smith, hank, two_asset from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput diff --git a/src/sequence_jacobian/blocks/test_parent_block.py b/src/sequence_jacobian/blocks/adhoc_test_parent_block.py similarity index 100% rename from src/sequence_jacobian/blocks/test_parent_block.py rename to src/sequence_jacobian/blocks/adhoc_test_parent_block.py From 655ed5afc87f87da273ed7f475b8d7c3d21ced4c Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 10 Aug 2021 19:32:16 -0500 Subject: [PATCH 202/288] removed extra lines from primitives.py --- src/sequence_jacobian/primitives.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index cefef3c..84fcb89 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -85,11 +85,6 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], calibration = make_steadystatedict(calibration)[inputs] kwargs['dissolve'] = dissolve - # TODO: does this respect remapping? - if self.name in dissolve and "solver" in self.ss_valid_input_kwargs: - kwargs["solver"] = "solved" - kwargs["unknowns"] = {k: v for k, v in calibration.items() if k in self.unknowns} - return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) def impulse_nonlinear(self, ss: SteadyStateDict, From c20f6be7aea2e397d1d171672edefe596ea613c6 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 11 Aug 2021 14:28:21 -0400 Subject: [PATCH 203/288] All tests pass. One test became irrelevant: we don't want to use Jacobians as simple dicts to initialize a DAG. --- tests/base/test_jacobian_dict_block.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 041bf59..1d59376 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -28,16 +28,11 @@ def test_jacobian_dict_block_impulses(rbc_dag): def test_jacobian_dict_block_combine(rbc_dag): - rbc_model, exogenous, unknowns, _, ss = rbc_dag + _, exogenous, _, _, ss = rbc_dag J_firm = rbc.firm.jacobian(ss, exogenous=exogenous) blocks_w_jdict = [rbc.household, J_firm, rbc.mkt_clearing] cblock_w_jdict = combine(blocks_w_jdict) - blocks_w_ndict = [rbc.household, J_firm.nesteddict, rbc.mkt_clearing] - cblock_w_ndict = combine(blocks_w_ndict) - - # Ensure that the JacobianDict and the raw nested dict were properly converted to JacobianDictBlocks after - # the use of combine + # Using `combine` converts JacobianDicts to JacobianDictBlocks assert isinstance(cblock_w_jdict._blocks_unsorted[1], JacobianDictBlock) - assert isinstance(cblock_w_ndict._blocks_unsorted[1], JacobianDictBlock) From 2aec2c27ffeaac6042a7d1f4ce474391dee2877c Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 11 Aug 2021 15:07:27 -0400 Subject: [PATCH 204/288] Workflow in nesting_test.py works with solved block. Had to make sure that dissolved block turns all self.unknowns into inputs. --- src/sequence_jacobian/blocks/solved_block.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index a297d94..a3a9099 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -48,7 +48,8 @@ def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={ # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort # Hence, we will cache that info upon first invocation of the steady_state self._sorted_indices_w_o_helpers = graph.block_sort(blocks) - self._sorted_indices_w_helpers = None # These indices are cached the first time steady state is evaluated + # These indices are cached the first time steady state is evaluated + self._sorted_indices_w_helpers = None self._required = graph.find_outputs_that_are_intermediate_inputs(blocks) # User-facing attributes for accessing blocks @@ -77,7 +78,7 @@ def _steady_state(self, calibration, dissolve=[], unknowns=None, helper_blocks=N consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): if self.name in dissolve: solver = "solved" - unknowns = {k: v for k, v in calibration.items() if k in unknowns} + unknowns = {k: v for k, v in calibration.items() if k in self.unknowns} # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices # accounting for HelperBlocks From 0053b7aba9a36095a7fe9637455cebd594571893 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 11 Aug 2021 15:19:12 -0400 Subject: [PATCH 205/288] Edited __init__.py. Don't load example models, do load main classes: SteadyStateDict, JacobianDict, ImpulseDict. --- src/sequence_jacobian/__init__.py | 6 ++++-- tests/base/test_transitional_dynamics.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index b09c1f0..97fbde7 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -2,13 +2,15 @@ from . import estimation, jacobian, nonlinear, utilities, devtools -from .models import rbc, krusell_smith, hank, two_asset - from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput from .blocks.solved_block import solved from .blocks.combined_block import combine, create_model from .blocks.support.simple_displacement import apply_function +from .steady_state.classes import SteadyStateDict +from .jacobian.classes import JacobianDict +from .blocks.support.impulse import ImpulseDict + from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 613453f..8c9fc94 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -2,8 +2,8 @@ import numpy as np -from sequence_jacobian import two_asset, combine, get_H_U -from sequence_jacobian import utilities as utils +from sequence_jacobian import combine +from sequence_jacobian.models import two_asset # TODO: Figure out a more robust way to check similarity of the linear and non-linear solution. From 9fcba39027f2fc9f833e2a33ca69432f73bac995 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 12 Aug 2021 13:03:17 -0500 Subject: [PATCH 206/288] stub of FactoredJacobianDict class, need to fill out --- src/sequence_jacobian/jacobian/classes.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index d5e1cdc..c4c2f81 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -7,6 +7,7 @@ from . import support from ..blocks.support.bijection import Bijection +from scipy.linalg import lu_factor, lu_solve class Jacobian(metaclass=ABCMeta): """An abstract base class encompassing all valid types representing Jacobians, which include @@ -455,6 +456,27 @@ def unpack(bigjac, outputs, inputs, T): return JacobianDict(jacdict, outputs, inputs) +class FactoredJacobianDict(JacobianDict): + def __init__(self, jacobian_dict: JacobianDict, T): + H_U = jacobian_dict.pack(T) + self.targets = jacobian_dict.outputs + self.unknowns = jacobian_dict.inputs + if len(self.targets) != len(self.unknowns): + raise ValueError('Trying to factor Jacobian Dict unequal number of inputs (unknowns)' + f' {self.unknowns} and outputs (targets) {self.targets}') + self.H_U_factored = lu_factor(H_U) + + def compose(self, J): + # take intersection of J outputs with self.targets + # then pack that reduced J into a matrix, then apply lu_solve, then unpack + pass + + def apply(self, x): + # take intersection of x entries with self.targets + # then pack (should be ImpulseDict?) into a vector, then apply lu_solve, then unpack + pass + + def ensure_valid_jacobiandict(d): """The valid structure of `d` is a Dict[str, Dict[str, Jacobian]], where calling `d[o][i]` yields a Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed From 1c6e22219de3ff94462611076864d2a5a697335d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 12 Aug 2021 16:27:30 -0400 Subject: [PATCH 207/288] test_solved_block.py illustrates how to use FactoredJacobianDict for SolvedBlocks --- src/sequence_jacobian/blocks/het_block.py | 3 +- src/sequence_jacobian/jacobian/classes.py | 50 ++++++++++++++++++++--- src/sequence_jacobian/utilities/misc.py | 34 --------------- test_solved_block.py | 30 ++++++++++++++ 4 files changed, 75 insertions(+), 42 deletions(-) create mode 100644 test_solved_block.py diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 03e66a6..d71536e 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -7,10 +7,9 @@ from ..primitives import Block from .. import utilities as utils from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict +from ..jacobian.classes import JacobianDict, verify_saved_jacobian from .support.bijection import Bijection from ..devtools.deprecate import rename_output_list_to_outputs -from ..utilities.misc import verify_saved_jacobian def het(exogenous, policy, backward, backward_init=None): diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index c4c2f81..c5af532 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -2,12 +2,13 @@ from abc import ABCMeta import copy +import warnings import numpy as np from . import support +from ..utilities.misc import factor, factored_solve from ..blocks.support.bijection import Bijection -from scipy.linalg import lu_factor, lu_solve class Jacobian(metaclass=ABCMeta): """An abstract base class encompassing all valid types representing Jacobians, which include @@ -455,21 +456,26 @@ def unpack(bigjac, outputs, inputs, T): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] return JacobianDict(jacdict, outputs, inputs) - class FactoredJacobianDict(JacobianDict): def __init__(self, jacobian_dict: JacobianDict, T): H_U = jacobian_dict.pack(T) self.targets = jacobian_dict.outputs self.unknowns = jacobian_dict.inputs if len(self.targets) != len(self.unknowns): - raise ValueError('Trying to factor Jacobian Dict unequal number of inputs (unknowns)' + raise ValueError('Trying to factor JacobianDict unequal number of inputs (unknowns)' f' {self.unknowns} and outputs (targets) {self.targets}') - self.H_U_factored = lu_factor(H_U) + self.H_U_factored = factor(H_U) - def compose(self, J): + def __repr__(self): + return f'<{type(self).__name__} unknowns={self.unknowns}, targets={self.targets}>' + + def compose(self, J: JacobianDict, T): # take intersection of J outputs with self.targets # then pack that reduced J into a matrix, then apply lu_solve, then unpack - pass + outputs = set(self.targets).intersection(set(J.outputs)) + B = J[[o for o in outputs]].pack(T) + X = -factored_solve(self.H_U_factored, B) + return JacobianDict.unpack(X, self.unknowns, J.inputs, T) def apply(self, x): # take intersection of x entries with self.targets @@ -501,3 +507,35 @@ def ensure_valid_jacobiandict(d): else: raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" f" values of type `Jacobian`.") + + +def verify_saved_jacobian(block_name, Js, outputs, inputs, T): + """Verify that pre-computed Jacobian has all the right outputs, inputs, and length.""" + if block_name not in Js.keys(): + # don't throw warning, this will happen often for simple blocks + return False + J = Js[block_name] + + if not isinstance(J, JacobianDict): + warnings.warn(f'Js[{block_name}] is not a JacobianDict.') + return False + + if not set(outputs).issubset(set(J.outputs)): + missing = set(outputs).difference(set(J.outputs)) + warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') + return False + + if not set(inputs).issubset(set(J.inputs)): + missing = set(inputs).difference(set(J.inputs)) + warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') + return False + + # Jacobian of simple blocks may have a sparse representation + if T is not None: + Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] + if T != Tsaved: + warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') + return False + + return True + diff --git a/src/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py index 8ff9ff0..dbaf4b4 100644 --- a/src/sequence_jacobian/utilities/misc.py +++ b/src/sequence_jacobian/utilities/misc.py @@ -4,8 +4,6 @@ import scipy.linalg import re import inspect -import warnings -from ..jacobian.classes import JacobianDict def make_tuple(x): @@ -143,38 +141,6 @@ def smart_zeros(n): else: return 0. - -def verify_saved_jacobian(block_name, Js, outputs, inputs, T): - """Verify that pre-computed Jacobian has all the right outputs, inputs, and length.""" - if block_name not in Js.keys(): - # don't throw warning, this will happen often for simple blocks - return False - J = Js[block_name] - - if not isinstance(J, JacobianDict): - warnings.warn(f'Js[{block_name}] is not a JacobianDict.') - return False - - if not set(outputs).issubset(set(J.outputs)): - missing = set(outputs).difference(set(J.outputs)) - warnings.warn(f'Js[{block_name}] misses required outputs {missing}.') - return False - - if not set(inputs).issubset(set(J.inputs)): - missing = set(inputs).difference(set(J.inputs)) - warnings.warn(f'Js[{block_name}] misses required inputs {missing}.') - return False - - # Jacobian of simple blocks may have a sparse representation - if T is not None: - Tsaved = J[J.outputs[0]][J.inputs[0]].shape[-1] - if T != Tsaved: - warnings.warn(f'Js[{block_name} has length {Tsaved}, but you asked for {T}') - return False - - return True - - '''Tools for taste shocks used in discrete choice problems''' diff --git a/test_solved_block.py b/test_solved_block.py new file mode 100644 index 0000000..672a039 --- /dev/null +++ b/test_solved_block.py @@ -0,0 +1,30 @@ +import numpy as np +from sequence_jacobian import simple, solved +from sequence_jacobian.steady_state.classes import SteadyStateDict +from sequence_jacobian.jacobian.classes import FactoredJacobianDict + + +@simple +def myblock(u, i): + res = 0.5 * i - u + return res + + +@solved(unknowns={'u': (-10.0, 10.0)}, targets=['res'], solver='brentq') +def myblock_solved(u, i): + res = 0.5 * i - u + return res + + +ss = SteadyStateDict({'u': 5, 'i': 10, 'res': 0.0}) + +# Compute jacobian of myblock_solved from scratch +Ja = myblock_solved.jacobian(ss, exogenous=['i'], T=5) + +# Compute jacobian of SolvedBlock using a pre-computed FactoredJacobian +J_d = myblock.jacobian(ss, exogenous=['u'], T=5) # square jac of underlying simple block +J_factored = FactoredJacobianDict(J_d, T=5) +J_c = myblock.jacobian(ss, exogenous=['i'], T=5) # jac of underlying simple block wrt inputs that are NOT unknowns +Jb = J_factored.compose(J_c, T=5) # obtain jac of unknown wrt to non-unknown inputs using factored jac + +assert np.allclose(Ja['u']['i'], Jb['u']['i']) \ No newline at end of file From 0d0f1ebb20f9f92551fffad1637c07397ca337a2 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 12 Aug 2021 16:15:30 -0500 Subject: [PATCH 208/288] working partial_jacobians, not yet adapted to SolvedBlock --- nesting_test.py | 26 ++++++++++--------- .../blocks/combined_block.py | 18 +++++-------- src/sequence_jacobian/primitives.py | 18 +++++++++++++ test_solved_block.py | 16 ++++++------ 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 93b3802..39091c1 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -138,21 +138,23 @@ def mkt_clearing(A, B, C, Y, G): unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal_solved(B, G, rb, Y, transfer, rho_B): - B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau +Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) -dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -calibration['rho_B'] = 0.8 +# @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +# def fiscal_solved(B, G, rb, Y, transfer, rho_B): +# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B +# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised +# tau = rev / Y +# return B_rule, rev, tau -ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +# dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +# calibration['rho_B'] = 0.8 + +# ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', +# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, +# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -assert all(np.allclose(ss0[k], ss[k]) for k in ss0) +# assert all(np.allclose(ss0[k], ss[k]) for k in ss0) # Partial Jacobians # J_ir = interest_rates.jacobian(ss, ['r', 'tau']) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 4a291cd..2909251 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -104,27 +104,23 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(irf_lin_partial_eq) - def partial_jacobians(self, ss, inputs=None, T=None, Js=None): + def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): """Calculate partial Jacobians (i.e. without forward accumulation) wrt `inputs` and outputs of other blocks.""" - if inputs is None: - inputs = self.inputs - # Add intermediate inputs; remove vector-valued inputs shocks = set(inputs) | self._required shocks -= set([k for k, v in ss.items() if np.size(v) > 1]) # Compute Jacobians along the DAG curlyJs = {} - kwargs = {"exogenous": shocks, "T": T, "Js": Js} for block in self.blocks: - # Don't remap here - curlyJ = block.jacobian(ss, **{k: kwargs[k] for k in utils.misc.input_kwarg_list(block._jacobian) - if k in kwargs}) + descendants = block.descendants if isinstance(block, Parent) else {block.name: None} + Js_block = {k: v for k, v in Js.items() if k in descendants} - # Don't return empty Jacobians - if curlyJ.outputs: - curlyJs[block.name] = curlyJ + # Don't remap here + curlyJ = block.partial_jacobians(ss, shocks & block.inputs, T, Js_block) + curlyJs.update(curlyJ) + return curlyJs def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 84fcb89..042a1c5 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -99,6 +99,23 @@ def impulse_linear(self, ss: SteadyStateDict, from a steady state `ss`.""" return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) + def partial_jacobians(self, ss, inputs=None, T=None, Js={}): + # TODO: annotate signature + if inputs is None: + inputs = self.inputs + + # if you have a J for this block that already has everything you need, use it + if (self.name in Js) and (inputs <= Js[self.name].inputs) and (self.outputs == Js[self.name].outputs): + return Js[self.name][:, inputs] + + # if it's a leaf, just call Jacobian method, include if nonzero + if not isinstance(self, Parent): + jac = self.jacobian(ss, inputs, T) + return {self.name: jac} if jac else {} + + # otherwise call child method with remapping (but not for Js, which are not remapped to top level) + return self.M @ self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, T, Js) + def jacobian(self, ss: SteadyStateDict, exogenous: List[str], T: Optional[int] = None, **kwargs) -> JacobianDict: @@ -158,6 +175,7 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], def remap(self, map): other = deepcopy(self) other.M = self.M @ Bijection(map) + # TODO: maybe we want to have an ._inputs and ._outputs that never changes, so that it can be used internally? other.inputs = other.M @ self.inputs other.outputs = other.M @ self.outputs if hasattr(self, 'input_list'): diff --git a/test_solved_block.py b/test_solved_block.py index 672a039..60fb2ea 100644 --- a/test_solved_block.py +++ b/test_solved_block.py @@ -6,25 +6,25 @@ @simple def myblock(u, i): - res = 0.5 * i - u + res = 0.5 * i(1) - u**2 - u(1) return res @solved(unknowns={'u': (-10.0, 10.0)}, targets=['res'], solver='brentq') def myblock_solved(u, i): - res = 0.5 * i - u + res = 0.5 * i(1) - u**2 - u(1) return res ss = SteadyStateDict({'u': 5, 'i': 10, 'res': 0.0}) # Compute jacobian of myblock_solved from scratch -Ja = myblock_solved.jacobian(ss, exogenous=['i'], T=5) +J1 = myblock_solved.jacobian(ss, exogenous=['i'], T=20) # Compute jacobian of SolvedBlock using a pre-computed FactoredJacobian -J_d = myblock.jacobian(ss, exogenous=['u'], T=5) # square jac of underlying simple block -J_factored = FactoredJacobianDict(J_d, T=5) -J_c = myblock.jacobian(ss, exogenous=['i'], T=5) # jac of underlying simple block wrt inputs that are NOT unknowns -Jb = J_factored.compose(J_c, T=5) # obtain jac of unknown wrt to non-unknown inputs using factored jac +J_u = myblock.jacobian(ss, exogenous=['u'], T=20) # square jac of underlying simple block +J_factored = FactoredJacobianDict(J_u, T=20) +J_i = myblock.jacobian(ss, exogenous=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns +J2 = J_factored.compose(J_i, T=20) # obtain jac of unknown wrt to non-unknown inputs using factored jac -assert np.allclose(Ja['u']['i'], Jb['u']['i']) \ No newline at end of file +assert np.allclose(J1['u']['i'], J2['u']['i']) \ No newline at end of file From f8327101b4f24f7e81a37b2f5af214261e06f7d1 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 12 Aug 2021 16:43:06 -0500 Subject: [PATCH 209/288] redid SolvedBlock so that it only solves a single child block, so that it does not have an embedded CombinedBlock anymore --- nesting_test.py | 24 +++--- src/sequence_jacobian/blocks/solved_block.py | 78 +++++++------------- src/sequence_jacobian/primitives.py | 6 ++ 3 files changed, 44 insertions(+), 64 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 39091c1..0acf331 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -140,21 +140,21 @@ def mkt_clearing(A, B, C, Y, G): Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) -# @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -# def fiscal_solved(B, G, rb, Y, transfer, rho_B): -# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B -# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised -# tau = rev / Y -# return B_rule, rev, tau +@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +def fiscal_solved(B, G, rb, Y, transfer, rho_B): + B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau -# dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -# calibration['rho_B'] = 0.8 +dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +calibration['rho_B'] = 0.8 -# ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', -# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, -# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -# assert all(np.allclose(ss0[k], ss[k]) for k in ss0) +assert all(np.allclose(ss0[k], ss[k]) for k in ss0) # Partial Jacobians # J_ir = interest_rates.jacobian(ss, ['r', 'tau']) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index a3a9099..e0cc36d 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -8,22 +8,12 @@ from ..jacobian.classes import JacobianDict -def solved(unknowns, targets, block_list=[], solver=None, solver_kwargs={}, name=""): - """Creates SolvedBlocks. Can be applied in two ways, both of which return a SolvedBlock: - - as @solved(unknowns=..., targets=...) decorator on a single SimpleBlock - - as function solved(blocklist=..., unknowns=..., targets=...) where blocklist - can be any list of blocks - """ - if block_list: - if not name: - name = f"{block_list[0].name}_to_{block_list[-1].name}_solved" - # ordinary call, not as decorator - return SolvedBlock(block_list, name, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) - else: - # call as decorator, return function of function - def singleton_solved_block(f): - return SolvedBlock([simple(f).rename(f.__name__ + '_inner')], f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) - return singleton_solved_block +def solved(unknowns, targets, solver=None, solver_kwargs={}, name=""): + """Convenience @solved(unknowns=..., targets=...) decorator on a single SimpleBlock""" + # call as decorator, return function of function + def singleton_solved_block(f): + return SolvedBlock(simple(f).rename(f.__name__ + '_inner'), f.__name__, unknowns, targets, solver=solver, solver_kwargs=solver_kwargs) + return singleton_solved_block class SolvedBlock(Block, Parent): @@ -39,54 +29,38 @@ class SolvedBlock(Block, Parent): nonlinear transition path such that all internal targets of the mini SHADE model are zero. """ - def __init__(self, blocks, name, unknowns, targets, solver=None, solver_kwargs={}): - # Store the actual blocks in ._blocks_unsorted, and use .blocks_w_helpers and .blocks to index from there. - self._blocks_unsorted = blocks + def __init__(self, block, name, unknowns, targets, solver=None, solver_kwargs={}): super().__init__() - # Upon instantiation, we only have enough information to conduct a sort ignoring HelperBlocks - # since we need a `calibration` to resolve cyclic dependencies when including HelperBlocks in a topological sort - # Hence, we will cache that info upon first invocation of the steady_state - self._sorted_indices_w_o_helpers = graph.block_sort(blocks) - # These indices are cached the first time steady state is evaluated - self._sorted_indices_w_helpers = None - self._required = graph.find_outputs_that_are_intermediate_inputs(blocks) - - # User-facing attributes for accessing blocks - # .blocks_w_helpers meant to only interface with steady_state functionality - # .blocks meant to interface with dynamic functionality (impulses and jacobian calculations) - self.blocks_w_helpers = None - self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices_w_o_helpers] - + self.block = block self.name = name self.unknowns = unknowns self.targets = targets self.solver = solver self.solver_kwargs = solver_kwargs - # initialize as parent - Parent.__init__(self, self.blocks) + Parent.__init__(self, [self.block]) - # need to have inputs and outputs!!! - self.outputs = (set.union(*(b.outputs for b in blocks)) | set(list(self.unknowns.keys()))) - set(self.targets) - self.inputs = set.union(*(b.inputs for b in blocks)) - self.outputs + # validate unknowns and targets + if not len(unknowns) == len(targets): + raise ValueError(f'Unknowns {set(unknowns)} and targets {set(targets)} different sizes in SolvedBlock {name}') + if not set(unknowns) <= block.inputs: + raise ValueError(f'Unknowns has element {set(unknowns) - block.inputs} not in inputs in SolvedBlock {name}') + if not set(targets) <= block.outputs: + raise ValueError(f'Targets has element {set(targets) - block.outputs} not in outputs in SolvedBlock {name}') + + # what are overall outputs and inputs? + self.outputs = block.outputs | set(unknowns) + self.inputs = block.inputs - set(unknowns) def __repr__(self): return f"" - def _steady_state(self, calibration, dissolve=[], unknowns=None, helper_blocks=None, solver=None, - consistency_check=False, ttol=1e-9, ctol=1e-9, verbose=False): + def _steady_state(self, calibration, dissolve=[], unknowns=None, solver=None, ttol=1e-9, ctol=1e-9, verbose=False): if self.name in dissolve: solver = "solved" unknowns = {k: v for k, v in calibration.items() if k in self.unknowns} - # If this is the first time invoking steady_state/solve_steady_state, cache the sorted indices - # accounting for HelperBlocks - if self._sorted_indices_w_helpers is None: - self._sorted_indices_w_helpers = graph.block_sort(self._blocks_unsorted, helper_blocks=helper_blocks, - calibration=calibration) - self.blocks_w_helpers = [self._blocks_unsorted[i] for i in self._sorted_indices_w_helpers] - # Allow override of unknowns/solver, if one wants to evaluate the SolvedBlock at a particular set of # unknown values akin to the steady_state method of Block if unknowns is None: @@ -94,17 +68,17 @@ def _steady_state(self, calibration, dissolve=[], unknowns=None, helper_blocks=N if solver is None: solver = self.solver - return super().solve_steady_state(calibration, unknowns, self.targets, solver=solver, - consistency_check=consistency_check, ttol=ttol, ctol=ctol, verbose=verbose) + return self.block.solve_steady_state(calibration, unknowns, self.targets, solver=solver, + ttol=ttol, ctol=ctol, verbose=verbose) def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): - return super().solve_impulse_nonlinear(ss, exogenous=exogenous, + return self.block.solve_impulse_nonlinear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), Js=Js, targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) def _impulse_linear(self, ss, exogenous, T=None, Js=None): - return super().solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), + return self.block.solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), T=T, Js=Js) @@ -116,7 +90,7 @@ def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): relevant_shocks = [i for i in self.inputs if i in exogenous] if relevant_shocks: - return super().solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), + return self.block.solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), T=T, outputs=outputs, Js=Js) else: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 042a1c5..99633b5 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -172,6 +172,12 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], blocks = self.blocks if hasattr(self, "blocks") else [self] return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) + def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): + if name is None: + name = self.name + "_solved" + from .blocks.solved_block import SolvedBlock + return SolvedBlock(self, name, unknowns, targets, solver, solver_kwargs) + def remap(self, map): other = deepcopy(self) other.M = self.M @ Bijection(map) From 250e6f230e9ca961c090fd4eab4e469954846043 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 12 Aug 2021 17:03:35 -0500 Subject: [PATCH 210/288] some fix for partial jacobians and implementation for SolvedBlock --- src/sequence_jacobian/blocks/solved_block.py | 12 +++++++++++- src/sequence_jacobian/primitives.py | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index e0cc36d..ee37dfc 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -5,7 +5,7 @@ from ..blocks.parent import Parent from ..utilities import graph -from ..jacobian.classes import JacobianDict +from ..jacobian.classes import JacobianDict, FactoredJacobianDict def solved(unknowns, targets, solver=None, solver_kwargs={}, name=""): @@ -95,3 +95,13 @@ def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): T=T, outputs=outputs, Js=Js) else: return JacobianDict({}) + + def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): + # call it on the child first + inner_Js = self.block.partial_jacobians(ss, inputs=(self.unknowns.keys() | inputs), T=T, Js=Js) + + # with these inner Js, also compute H_U and factorize + H_U = self.block.jacobian(ss, inputs=self.unknowns.keys(), T=T, outputs=self.targets.keys(), Js=inner_Js) + H_U_factored = FactoredJacobianDict(H_U, T) + + return {**inner_Js, self.name: H_U_factored} diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 99633b5..c306ee8 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -113,8 +113,11 @@ def partial_jacobians(self, ss, inputs=None, T=None, Js={}): jac = self.jacobian(ss, inputs, T) return {self.name: jac} if jac else {} - # otherwise call child method with remapping (but not for Js, which are not remapped to top level) - return self.M @ self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, T, Js) + # otherwise call child method with remapping (and remap your own but none of the child Js) + partial = self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, T, Js) + if self.name in partial: + partial[self.name] = self.M @ partial[self.name] + return partial def jacobian(self, ss: SteadyStateDict, exogenous: List[str], From 0ff4b381acab34551edf67219d571d98949bfe1c Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 12 Aug 2021 18:37:37 -0400 Subject: [PATCH 211/288] Consolidate gains: all tests pass. --- src/sequence_jacobian/blocks/combined_block.py | 2 +- src/sequence_jacobian/blocks/simple_block.py | 8 ++++---- src/sequence_jacobian/jacobian/classes.py | 5 ++--- src/sequence_jacobian/models/two_asset.py | 5 +++-- test_solved_block.py => tests/base/test_solved_block.py | 0 5 files changed, 10 insertions(+), 10 deletions(-) rename test_solved_block.py => tests/base/test_solved_block.py (100%) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 2909251..a900566 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -123,7 +123,7 @@ def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): return curlyJs - def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js=None): + def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js={}): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at a steady state, `ss`""" if outputs is not None: diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 96ab3a8..daf8735 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -9,7 +9,7 @@ from .support.bijection import Bijection from ..primitives import Block from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix +from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix, verify_saved_jacobian from ..utilities import misc '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -69,7 +69,7 @@ def _impulse_nonlinear(self, ss, exogenous): def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) - def _jacobian(self, ss, exogenous=None, T=None, Js=None): + def _jacobian(self, ss, exogenous=None, T=None, Js={}): """Assemble nested dict of Jacobians Parameters @@ -97,8 +97,8 @@ def _jacobian(self, ss, exogenous=None, T=None, Js=None): relevant_shocks = [i for i in self.inputs if i in exogenous] # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js is not None: - if misc.verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): + if Js: + if verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): return Js[self.name] # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index c5af532..c3a9c5c 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -472,9 +472,8 @@ def __repr__(self): def compose(self, J: JacobianDict, T): # take intersection of J outputs with self.targets # then pack that reduced J into a matrix, then apply lu_solve, then unpack - outputs = set(self.targets).intersection(set(J.outputs)) - B = J[[o for o in outputs]].pack(T) - X = -factored_solve(self.H_U_factored, B) + Jsub = J[[o for o in self.targets if o in J.outputs]].pack(T) + X = -factored_solve(self.H_U_factored, Jsub) return JacobianDict.unpack(X, self.unknowns, J.inputs, T) def apply(self, x): diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 9948f1c..241cb45 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -7,6 +7,7 @@ from ..blocks.het_block import het, hetoutput from ..blocks.solved_block import solved from ..blocks.support.simple_displacement import apply_function +from ..blocks.combined_block import combine '''Part 1: HA block''' @@ -420,5 +421,5 @@ def arbitrage_solved(div, p, r): return equity -production_solved = solved(block_list=[labor, investment], unknowns={'Q': 1., 'K': 10.}, - targets=['inv', 'val'], solver="broyden_custom") +production = combine([labor, investment]) +production_solved = production.solved(unknowns={'Q': 1., 'K': 10.}, targets=['inv', 'val'], solver="broyden_custom") diff --git a/test_solved_block.py b/tests/base/test_solved_block.py similarity index 100% rename from test_solved_block.py rename to tests/base/test_solved_block.py From d76829597bb492c16c9fd8c1b0d1e3c0dea85f24 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 13 Aug 2021 16:28:49 -0500 Subject: [PATCH 212/288] in midst of major rewriting of jacobian methods, fixed some SteadyStateDict stuff --- nesting_test.py | 4 +- .../blocks/combined_block.py | 54 +++------ src/sequence_jacobian/blocks/het_block.py | 21 ++-- src/sequence_jacobian/blocks/simple_block.py | 109 ++++++------------ src/sequence_jacobian/blocks/solved_block.py | 26 ++--- src/sequence_jacobian/primitives.py | 72 +++++++++--- src/sequence_jacobian/steady_state/classes.py | 53 ++++----- src/sequence_jacobian/steady_state/drivers.py | 2 +- 8 files changed, 148 insertions(+), 193 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 0acf331..b1cc72b 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -131,8 +131,8 @@ def mkt_clearing(A, B, C, Y, G): hh = combine([household, income_state_vars, asset_state_vars], name='HH') dag = sj.create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') -calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 11, - 'amin': 0.0, 'amax': 1000, 'nA': 500, 'Gamma': 0.0, 'transfer': 0.143} +calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, + 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143} ss0 = dag.solve_steady_state(calibration, solver='hybr', unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index a900566..ebc2151 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -69,16 +69,14 @@ def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) blocks_all = self.blocks + helper_blocks - ss_partial_eq_toplevel = deepcopy(calibration) - ss_partial_eq_internal = {} + ss = deepcopy(calibration) for i in topsorted: # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children inner_dissolve = [k for k in dissolve if self.descendants[k] == blocks_all[i].name] - outputs = blocks_all[i].steady_state(ss_partial_eq_toplevel, dissolve=inner_dissolve, **kwargs) - ss_partial_eq_toplevel.update(outputs.toplevel) - if outputs.internal: - ss_partial_eq_internal.update(outputs.internal) - return SteadyStateDict(ss_partial_eq_toplevel, internal=ss_partial_eq_internal) + outputs = blocks_all[i].steady_state(ss, dissolve=inner_dissolve, **kwargs) + ss.update(outputs) + + return ss def _impulse_nonlinear(self, ss, exogenous, **kwargs): """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from @@ -104,11 +102,12 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(irf_lin_partial_eq) - def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): + def _partial_jacobians(self, ss, inputs, outputs, T, Js): """Calculate partial Jacobians (i.e. without forward accumulation) wrt `inputs` and outputs of other blocks.""" # Add intermediate inputs; remove vector-valued inputs - shocks = set(inputs) | self._required - shocks -= set([k for k, v in ss.items() if np.size(v) > 1]) + vector_valued = set([k for k, v in ss.items() if np.size(v) > 1]) + inputs = (inputs | self._required) - vector_valued + outputs = (outputs | self._required) - vector_valued # Compute Jacobians along the DAG curlyJs = {} @@ -116,40 +115,25 @@ def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): descendants = block.descendants if isinstance(block, Parent) else {block.name: None} Js_block = {k: v for k, v in Js.items() if k in descendants} - # Don't remap here - curlyJ = block.partial_jacobians(ss, shocks & block.inputs, T, Js_block) - + curlyJ = block.partial_jacobians(ss, inputs & block.inputs, outputs & block.outputs, T, Js_block) curlyJs.update(curlyJ) return curlyJs - def _jacobian(self, ss, exogenous=None, T=None, outputs=None, Js={}): + def _jacobian(self, ss, inputs, outputs, T, Js={}): """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at a steady state, `ss`""" - if outputs is not None: - # if list of outputs provided, we need to obtain these and 'required' along the way - alloutputs = set(outputs) | self._required - else: - # otherwise, set to None, implies default behavior of obtaining all outputs in curlyJs - alloutputs = None - - # Compute all partial Jacobians - curlyJs = self.partial_jacobians(ss, inputs=exogenous, T=T, Js=Js) + # _partial_jacobians should calculate partial jacobians with exactly the inputs and outputs we want + Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) # Forward accumulate partial Jacobians - out = JacobianDict.identity(exogenous) - for curlyJ in curlyJs.values(): - if alloutputs is not None: - # don't accumulate derivatives we don't need or care about - curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] - out.update(curlyJ.compose(out)) - - if outputs is not None: - # don't return derivatives that we don't care about (even if required them above) - return out[[k for k in outputs if k in out.outputs]] - else: - return out + total_Js = JacobianDict.identity(inputs) + for block in self.blocks: + if block.name in Js: + J = Js[block.name] + total_Js.update(J @ total_Js) + return total_Js[outputs, :] def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, sort_blocks=False, **kwargs): diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index d71536e..43b7b65 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -203,16 +203,16 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars """ - ss = copy.deepcopy(calibration) + ss = copy.deepcopy(calibration.toplevel) # extract information from calibration - Pi = calibration[self.exogenous] - grid = {k: calibration[k+'_grid'] for k in self.policy} - D_seed = calibration.get('D', None) - pi_seed = calibration.get(self.exogenous + '_seed', None) + Pi = ss[self.exogenous] + grid = {k: ss[k+'_grid'] for k in self.policy} + D_seed = ss.get('D', None) + pi_seed = ss.get(self.exogenous + '_seed', None) # run backward iteration - sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) + sspol = self.policy_ss(ss, tol=backward_tol, maxit=backward_maxit) ss.update(sspol) # run forward iteration @@ -231,7 +231,8 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregate_hetoutputs = {} ss.update({**hetoutputs, **aggregate_hetoutputs}) - return SteadyStateDict(ss, internal=self) + return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, + {self.name: {k: ss[k] for k in ss if k in self.internal}}) def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, @@ -388,12 +389,6 @@ def _jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, J relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] - # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js is not None: - outputs_cap = [o.capitalize() for o in outputs] - if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): - return Js[self.name] - # step 0: preliminary processing of steady state (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index daf8735..bcb83d2 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -34,14 +34,12 @@ def production(Z, K, L, alpha): Key methods are .ss, .td, and .jac, like HetBlock. """ - # TODO: get rid of .input_list because it serves no function def __init__(self, f): super().__init__() self.f = f self.name = f.__name__ - self.input_list = misc.input_list(f) self.output_list = misc.output_list(f) - self.inputs = set(self.input_list) + self.inputs = set(misc.input_list(f)) self.outputs = set(self.output_list) def __repr__(self): @@ -69,78 +67,39 @@ def _impulse_nonlinear(self, ss, exogenous): def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) - def _jacobian(self, ss, exogenous=None, T=None, Js={}): - """Assemble nested dict of Jacobians - - Parameters - ---------- - ss : dict, - steady state values - exogenous : list of str, optional - names of input variables to differentiate wrt; if omitted, assume all inputs - T : int, optional - number of time periods for explicit T*T Jacobian - if omitted, more efficient SimpleSparse objects returned - Js : dict of {str: JacobianDict}, optional - pre-computed Jacobians - - Returns - ------- - J : dict of {str: dict of {str: array(T,T)}} - J[o][i] for output o and input i gives Jacobian of o with respect to i - This Jacobian is a SimpleSparse object or, if T specific, a T*T matrix, omitted by convention if zero - """ - - if exogenous is None: - exogenous = list(self.inputs) - - relevant_shocks = [i for i in self.inputs if i in exogenous] - - # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js: - if verify_saved_jacobian(self.name, Js, self.outputs, relevant_shocks, T): - return Js[self.name] - - # If none of the shocks passed in shock_list are relevant to this block (i.e. none of the shocks - # are an input into the block), then return an empty dict - if not relevant_shocks: - return JacobianDict({}) - else: - invertedJ = {shock_name: {} for shock_name in relevant_shocks} - - # Loop over all inputs/shocks which we want to differentiate with respect to - for shock in relevant_shocks: - invertedJ[shock] = compute_single_shock_curlyJ(self.f, ss, shock) - - # Because we computed the Jacobian of all outputs with respect to each shock (invertedJ[i][o]), - # we need to loop back through to have J[o][i] to map for a given output `o`, shock `i`, - # the Jacobian curlyJ^{o,i}. - J = {o: {} for o in self.output_list} - for o in self.output_list: - for i in relevant_shocks: - # Keep zeros, so we can inspect supplied Jacobians for completeness - if not invertedJ[i][o] or invertedJ[i][o].iszero: - J[o][i] = ZeroMatrix() - else: - if T is not None: - J[o][i] = invertedJ[i][o].matrix(T) - else: - J[o][i] = invertedJ[i][o] - - return JacobianDict(J, name=self.name) - - -def compute_single_shock_curlyJ(f, steady_state_dict, shock_name): - """Find the Jacobian of the function `f` with respect to a single shocked argument, `shock_name`""" - input_args = {i: ignore(steady_state_dict[i]) for i in misc.input_list(f)} - input_args[shock_name] = AccumulatedDerivative(f_value=steady_state_dict[shock_name]) - - J = {o: {} for o in misc.output_list(f)} - for o, o_name in zip(misc.make_tuple(f(**input_args)), misc.output_list(f)): - if isinstance(o, AccumulatedDerivative): - J[o_name] = SimpleSparse(o.elements) - - return J + def _jacobian(self, ss, inputs, outputs, T): + invertedJ = {i: {} for i in inputs} + + # Loop over all inputs/shocks which we want to differentiate with respect to + for i in inputs: + invertedJ[i] = self.compute_single_shock_J(ss, i) + + # Because we computed the Jacobian of all outputs with respect to each shock (invertedJ[i][o]), + # we need to loop back through to have J[o][i] to map for a given output `o`, shock `i`, + # the Jacobian curlyJ^{o,i}. + J = {o: {} for o in self.outputs} + for o in self.outputs: + for i in inputs: + # Keep zeros, so we can inspect supplied Jacobians for completeness + if not invertedJ[i][o] or invertedJ[i][o].iszero: + J[o][i] = ZeroMatrix() + else: + J[o][i] = invertedJ[i][o] + + print(J) + + return JacobianDict(J, name=self.name)[outputs, :] + + def compute_single_shock_J(self, ss, i): + input_args = {i: ignore(ss[i]) for i in self.inputs} + input_args[i] = AccumulatedDerivative(f_value=ss[i]) + + J = {o: {} for o in self.output_list} + for o, o_name in zip(misc.make_tuple(self.f(**input_args)), self.output_list): + if isinstance(o, AccumulatedDerivative): + J[o_name] = SimpleSparse(o.elements) + + return J def make_impulse_uniform_length(out, output_list): diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index ee37dfc..317f66c 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -29,7 +29,7 @@ class SolvedBlock(Block, Parent): nonlinear transition path such that all internal targets of the mini SHADE model are zero. """ - def __init__(self, block, name, unknowns, targets, solver=None, solver_kwargs={}): + def __init__(self, block: Block, name, unknowns, targets, solver=None, solver_kwargs={}): super().__init__() self.block = block @@ -82,26 +82,16 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), T=T, Js=Js) - def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None): - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None: - outputs = list(self.outputs) - relevant_shocks = [i for i in self.inputs if i in exogenous] - - if relevant_shocks: - return self.block.solve_jacobian(ss, relevant_shocks, unknowns=list(self.unknowns.keys()), - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, outputs=outputs, Js=Js) - else: - return JacobianDict({}) - - def _partial_jacobians(self, ss, inputs=None, T=None, Js={}): + def _jacobian(self, ss, inputs, outputs, T, Js): + return self.block.solve_jacobian(ss, set(self.unknowns), set(self.targets), inputs, outputs, T, Js) + + def _partial_jacobians(self, ss, inputs, outputs, T, Js={}): # call it on the child first - inner_Js = self.block.partial_jacobians(ss, inputs=(self.unknowns.keys() | inputs), T=T, Js=Js) + inner_Js = self.block.partial_jacobians(ss, inputs=(self.unknowns.keys() | inputs), + outputs=(self.targets.keys() | outputs), T=T, Js=Js) # with these inner Js, also compute H_U and factorize - H_U = self.block.jacobian(ss, inputs=self.unknowns.keys(), T=T, outputs=self.targets.keys(), Js=inner_Js) + H_U = self.block.jacobian(ss, inputs=self.unknowns.keys(), outputs=self.targets.keys(), T=T, Js=inner_Js) H_U_factored = FactoredJacobianDict(H_U, T) return {**inner_Js, self.name: H_U_factored} diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index c306ee8..e65d98c 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -1,6 +1,7 @@ """Primitives to provide clarity and structure on blocks/models work""" import abc +import numpy as np from abc import ABCMeta as NativeABCMeta from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List @@ -10,7 +11,7 @@ from .steady_state.support import provide_solver_default from .nonlinear import td_solve from .jacobian.drivers import get_impulse, get_G -from .steady_state.classes import SteadyStateDict, UserProvidedSS, make_steadystatedict +from .steady_state.classes import SteadyStateDict, UserProvidedSS from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict from .blocks.support.bijection import Bijection @@ -82,7 +83,7 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], for k in dissolve: inputs |= self.get_attribute(k, 'unknowns').keys() - calibration = make_steadystatedict(calibration)[inputs] + calibration = SteadyStateDict(calibration)[inputs] kwargs['dissolve'] = dissolve return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) @@ -99,31 +100,47 @@ def impulse_linear(self, ss: SteadyStateDict, from a steady state `ss`.""" return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def partial_jacobians(self, ss, inputs=None, T=None, Js={}): + def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): # TODO: annotate signature if inputs is None: inputs = self.inputs + if outputs is None: + outputs = self.outputs + inputs, outputs = set(inputs), set(outputs) # if you have a J for this block that already has everything you need, use it - if (self.name in Js) and (inputs <= Js[self.name].inputs) and (self.outputs == Js[self.name].outputs): - return Js[self.name][:, inputs] + # TODO: add check for T, maybe look at verify_saved_jacobian for ideas? + if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): + return Js[self.name][outputs, inputs] # if it's a leaf, just call Jacobian method, include if nonzero if not isinstance(self, Parent): - jac = self.jacobian(ss, inputs, T) + jac = self.jacobian(ss, inputs, outputs, T) return {self.name: jac} if jac else {} # otherwise call child method with remapping (and remap your own but none of the child Js) - partial = self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, T, Js) + partial = self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T, Js) if self.name in partial: partial[self.name] = self.M @ partial[self.name] return partial - def jacobian(self, ss: SteadyStateDict, - exogenous: List[str], - T: Optional[int] = None, **kwargs) -> JacobianDict: - """Calculate a partial equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`.""" - return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ exogenous, T=T, **kwargs) + def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, + T: Optional[int] = None, Js={}) -> JacobianDict: + """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" + inputs, outputs = self.default_inputs_outputs(inputs, outputs) + inputs, outputs = set(inputs), set(outputs) + + # if you have a J for this block that has everything you need, use it + if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): + return Js[self.name][outputs, inputs] + + # if it's a leaf, call Jacobian method, don't supply Js + if not isinstance(self, Parent): + return self._jacobian(ss, inputs, outputs, T) + + # otherwise remap own J (currently needed for SolvedBlock only) + Js = {**{k: v for k, v in Js.items() if k != self.name}, self.name: self.M.inv @ Js[self.name]} + return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], @@ -163,17 +180,29 @@ def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) return ImpulseDict(irf_lin_gen_eq) - def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], - exogenous: List[str], - unknowns: List[str], targets: List[str], - T: Optional[int] = None, + def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], targets: List[str], + inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = None, **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - return get_G(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) + # TODO: do we really want this? is T just optional because we want it to come after outputs in docstring? + if T is None: + T = 300 + + inputs, outputs = self.default_inputs_outputs(inputs, outputs) + inputs, unknowns, targets = list(inputs), list(unknowns), list(targets) + + Js = self.partial_jacobians(ss, inputs | set(unknowns), outputs | set(targets), T, Js) + + H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) + H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) + U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, inputs, T) + + from . import combine + self_with_unknowns = combine(U_Z, self) + return self_with_unknowns.jacobian(ss, inputs, outputs, T, Js) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: @@ -199,3 +228,10 @@ def rename(self, name): renamed = deepcopy(self) renamed.name = name return renamed + + def default_inputs_outputs(self, inputs, outputs): + if inputs is None: + inputs = self.inputs + if outputs is None: + outputs = self.outputs + return inputs, outputs diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index 9f34d7a..63df6db 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -10,10 +10,18 @@ Array = Any class SteadyStateDict: + # TODO: should this just subclass dict so we can avoid a lot of boilerplate? + # Really this is just a top-level dict (with all the usual functionality) with "internal" bolted on + def __init__(self, data, internal=None): - self.toplevel = {} - self.internal = {} - self.update(data, internal_namespaces=internal) + if isinstance(data, SteadyStateDict): + if internal is not None: + raise ValueError('Supplying SteadyStateDict and also internal to constructor not allowed') + self.toplevel = data + self.internal = {} + + self.toplevel: dict = data + self.internal: dict = {} if internal is None else internal def __repr__(self): if self.internal: @@ -29,7 +37,7 @@ def __getitem__(self, k): return self.toplevel[k] else: try: - return {ki: self.toplevel[ki] for ki in k} + return SteadyStateDict({ki: self.toplevel[ki] for ki in k}) except TypeError: raise TypeError(f'Key {k} needs to be a string or an iterable (list, set, etc) of strings') @@ -43,11 +51,14 @@ def __matmul__(self, x): new.toplevel = x @ self.toplevel return new else: - NotImplemented + return NotImplemented def __rmatmul__(self, x): return self.__matmul__(x) + def __len__(self): + return len(self.toplevel) + def keys(self): return self.toplevel.keys() @@ -57,34 +68,14 @@ def values(self): def items(self): return self.toplevel.items() - def update(self, data, internal_namespaces=None): - if isinstance(data, SteadyStateDict): - self.internal.update(deepcopy(data.internal)) - self.toplevel.update(deepcopy(data.toplevel)) + def update(self, ssdict): + if isinstance(ssdict, SteadyStateDict): + self.toplevel.update(ssdict.toplevel) + self.internal.update(ssdict.internal) else: - toplevel = deepcopy(data) - if internal_namespaces is not None: - # Construct the internal namespace from the Block object, if a Block is provided - if hasattr(internal_namespaces, "internal"): - internal_namespaces = {internal_namespaces.name: {k: v for k, v in deepcopy(data).items() if k in - internal_namespaces.internal}} - - # Remove the internal data from `data` if it's there - for internal_dict in internal_namespaces.values(): - toplevel = dict_diff(toplevel, internal_dict) - - self.toplevel.update(toplevel) - self.internal.update(internal_namespaces) - else: - self.toplevel.update(toplevel) + self.toplevel.update(dict(ssdict)) def difference(self, data_to_remove): - return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), internal=deepcopy(self.internal)) + return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internal)) UserProvidedSS = Dict[str, Union[Real, Array]] - -def make_steadystatedict(ss: Union[SteadyStateDict, UserProvidedSS]) -> SteadyStateDict: - if not isinstance(ss, SteadyStateDict): - return SteadyStateDict(ss) - else: - return ss diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index a5c0873..549937b 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -129,7 +129,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=ttol, verbose=verbose, fragile=fragile) - + # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check and helper_blocks: # Add the unknowns not handled by helpers into the DAG to be checked. From c1732d6dbd1b8bf944c867cc304b05da2bff3083 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 13 Aug 2021 17:22:47 -0500 Subject: [PATCH 213/288] in progress refactoring of jacobians --- nesting_test.py | 28 ++++++++++---------- src/sequence_jacobian/blocks/het_block.py | 24 ++++++++++------- src/sequence_jacobian/blocks/simple_block.py | 1 + src/sequence_jacobian/blocks/solved_block.py | 4 +-- src/sequence_jacobian/jacobian/classes.py | 25 ++++++++++------- src/sequence_jacobian/primitives.py | 4 +-- 6 files changed, 48 insertions(+), 38 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index b1cc72b..370dc93 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -138,23 +138,23 @@ def mkt_clearing(A, B, C, Y, G): unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) +#Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) -@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal_solved(B, G, rb, Y, transfer, rho_B): - B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau +# @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +# def fiscal_solved(B, G, rb, Y, transfer, rho_B): +# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B +# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised +# tau = rev / Y +# return B_rule, rev, tau -dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -calibration['rho_B'] = 0.8 +# dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +# calibration['rho_B'] = 0.8 -ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +# ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', +# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, +# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -assert all(np.allclose(ss0[k], ss[k]) for k in ss0) +# assert all(np.allclose(ss0[k], ss[k]) for k in ss0) # Partial Jacobians # J_ir = interest_rates.jacobian(ss, ['r', 'tau']) @@ -191,7 +191,7 @@ def fiscal_solved(B, G, rb, Y, transfer, rho_B): # J_all2 = J_int.compose(J_hh) -# G = dag.solve_jacobian(ss, exogenous=['r'], unknowns=['Y'], targets=['asset_mkt'], T=300) +G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) # td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, # unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 43b7b65..50689f6 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -87,7 +87,9 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used # in utils.graph.block_sort to topologically sort blocks along the DAG # according to their aggregate outputs and inputs. + # TODO: go back from capitalize to upper!!! (ask Michael first) self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} + self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} self.inputs.remove(exogenous + '_p') self.inputs.add(exogenous) @@ -354,7 +356,8 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - def _jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, Js=None, h=1E-4): + def _jacobian(self, ss, inputs, outputs, T, h=1E-4): + # TODO: h is unusable for now, figure out how to suggest options """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. Parameters @@ -378,16 +381,15 @@ def _jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, J J : dict of {str: dict of {str: array(T,T)}} J[o][i] for output o and input i gives T*T Jacobian of o with respect to i """ - # The default set of outputs are all outputs of the backward iteration function - # except for the backward iteration variables themselves - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None or output_list is None: - outputs = self.non_back_iter_outputs - else: - outputs = rename_output_list_to_outputs(outputs=outputs, output_list=output_list) + outputs = self.M_outputs.inv @ outputs # horrible + print('HERE ARE THE OUTPUTS') + print(outputs) + + print(self.M_outputs) - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] + # TODO: this is one instance of us letting people supply inputs that aren't actually inputs + # This behavior should lead to an error instead (probably should be handled at top level) + relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in inputs] # step 0: preliminary processing of steady state (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) @@ -422,6 +424,7 @@ def _jacobian(self, ss, exogenous=None, T=300, outputs=None, output_list=None, J return JacobianDict(J, name=self.name) def add_hetinput(self, hetinput, overwrite=False, verbose=True): + # TODO: serious violation, this is mutating the block """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process inputs through the hetinput function. @@ -487,6 +490,7 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) # Modify the HetBlock's outputs to include the aggregated hetoutputs self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) + self.M_outputs = Bijection({o: o.capitalize() for o in self.hetoutput_outputs}) @ self.M_outputs self.internal |= self.hetoutput_outputs diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index bcb83d2..d4185eb 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -87,6 +87,7 @@ def _jacobian(self, ss, inputs, outputs, T): J[o][i] = invertedJ[i][o] print(J) + print(JacobianDict(J, name=self.name)) return JacobianDict(J, name=self.name)[outputs, :] diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 317f66c..ce7d41f 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -87,8 +87,8 @@ def _jacobian(self, ss, inputs, outputs, T, Js): def _partial_jacobians(self, ss, inputs, outputs, T, Js={}): # call it on the child first - inner_Js = self.block.partial_jacobians(ss, inputs=(self.unknowns.keys() | inputs), - outputs=(self.targets.keys() | outputs), T=T, Js=Js) + inner_Js = self.block.partial_jacobians(ss, inputs=(set(self.unknowns) | inputs), + outputs=(set(self.targets) | outputs), T=T, Js=Js) # with these inner Js, also compute H_U and factorize H_U = self.block.jacobian(ss, inputs=self.unknowns.keys(), outputs=self.targets.keys(), T=T, Js=inner_Js) diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index c3a9c5c..e56a277 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -291,6 +291,10 @@ def __init__(self, nesteddict, outputs=None, inputs=None, name=None): inputs.extend(list(v)) inputs = deduplicate(inputs) + if not outputs or not inputs: + outputs = [] + inputs = [] + self.outputs = list(outputs) self.inputs = list(inputs) if name is None: @@ -487,22 +491,23 @@ def ensure_valid_jacobiandict(d): Jacobian of type Jacobian mapping sequences of `i` to sequences of `o`. The null type for `d` is assumed to be {}, which is permitted the empty version of a valid nested dict.""" - if d != {} and not isinstance(d, JacobianDict): + if d and not isinstance(d, JacobianDict): # Assume it's sufficient to just check one of the keys if not isinstance(next(iter(d.keys())), str): raise ValueError(f"The dict argument {d} must have keys with type `str` to indicate `output` names.") jac_o_dict = next(iter(d.values())) if isinstance(jac_o_dict, dict): - if not isinstance(next(iter(jac_o_dict.keys())), str): - raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" - f" `input` names.") - jac_o_i = next(iter(jac_o_dict.values())) - if not isinstance(jac_o_i, Jacobian): - raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") - else: - if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: - raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") + if jac_o_dict: + if not isinstance(next(iter(jac_o_dict.keys())), str): + raise ValueError(f"The values of the dict argument {d} must be dicts with keys of type `str` to indicate" + f" `input` names.") + jac_o_i = next(iter(jac_o_dict.values())) + if not isinstance(jac_o_i, Jacobian): + raise ValueError(f"The dict argument {d}'s values must be dicts with values of type `Jacobian`.") + else: + if isinstance(jac_o_i, np.ndarray) and np.shape(jac_o_i)[0] != np.shape(jac_o_i)[1]: + raise ValueError(f"The Jacobians in {d} must be square matrices of type `Jacobian`.") else: raise ValueError(f"The argument {d} must be of type `dict`, with keys of type `str` and" f" values of type `Jacobian`.") diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index e65d98c..4a04d75 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -182,7 +182,7 @@ def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = None, + Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous @@ -194,7 +194,7 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], inputs, outputs = self.default_inputs_outputs(inputs, outputs) inputs, unknowns, targets = list(inputs), list(unknowns), list(targets) - Js = self.partial_jacobians(ss, inputs | set(unknowns), outputs | set(targets), T, Js) + Js = self.partial_jacobians(ss, set(inputs) | set(unknowns), set(outputs) | set(targets), T, Js) H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) From f63ce39af900da25b869bfd6d09001a4353a2ba2 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Mon, 16 Aug 2021 14:48:12 -0500 Subject: [PATCH 214/288] in progress refactoring, G calculation in nesting_test seems to work now --- nesting_test.py | 17 ++- .../auxiliary_blocks/jacobiandict_block.py | 12 +- .../blocks/combined_block.py | 19 ++- src/sequence_jacobian/blocks/simple_block.py | 5 +- .../blocks/support/bijection.py | 4 + src/sequence_jacobian/jacobian/classes.py | 28 ++-- src/sequence_jacobian/primitives.py | 8 +- src/sequence_jacobian/utilities/function.py | 73 +++++++++ src/sequence_jacobian/utilities/misc.py | 41 +----- .../utilities/ordered_set.py | 138 ++++++++++++++++++ tests/utils/test_function.py | 24 +++ tests/utils/test_ordered_set.py | 57 ++++++++ 12 files changed, 355 insertions(+), 71 deletions(-) create mode 100644 src/sequence_jacobian/utilities/function.py create mode 100644 src/sequence_jacobian/utilities/ordered_set.py create mode 100644 tests/utils/test_function.py create mode 100644 tests/utils/test_ordered_set.py diff --git a/nesting_test.py b/nesting_test.py index 370dc93..f7b3845 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -106,11 +106,18 @@ def interest_rates(r): return rpost, rb +# @simple +# def fiscal(B, G, rb, Y, transfer): +# rev = rb * B + G + transfer # revenue to be raised +# tau = rev / Y +# return rev, tau + @simple -def fiscal(B, G, rb, Y, transfer): - rev = rb * B + G + transfer # revenue to be raised +def fiscal(B, G, rb, Y, transfer, rho_B): + B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised tau = rev / Y - return rev, tau + return B_rule, rev, tau @simple @@ -133,6 +140,7 @@ def mkt_clearing(A, B, C, Y, G): calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143} +calibration['rho_B'] = 0.8 ss0 = dag.solve_steady_state(calibration, solver='hybr', unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, @@ -148,7 +156,6 @@ def mkt_clearing(A, B, C, Y, G): # return B_rule, rev, tau # dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -# calibration['rho_B'] = 0.8 # ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', # unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, @@ -191,7 +198,7 @@ def mkt_clearing(A, B, C, Y, G): # J_all2 = J_int.compose(J_hh) -G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) +G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) # td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, # unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index 227779d..95c8ab9 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -19,8 +19,10 @@ def impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: return self.jacobian(list(exogenous.keys())).apply(exogenous) - def jacobian(self, exogenous: List[str] = None, **kwargs) -> JacobianDict: - if exogenous is None: - return JacobianDict(self.nesteddict) - else: - return self[:, exogenous] + def _jacobian(self, ss, inputs, outputs, T) -> JacobianDict: + # TODO: T should be an attribute of JacobianDict + if not inputs <= self.inputs: + raise KeyError(f'Asking JacobianDictBlock for {inputs - self.inputs}, which are among its inputs {self.inputs}') + if not outputs <= self.outputs: + raise KeyError(f'Asking JacobianDictBlock for {outputs - self.outputs}, which are among its outputs {self.outputs}') + return self[outputs, inputs] diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index ebc2151..faf3456 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -126,14 +126,23 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): # _partial_jacobians should calculate partial jacobians with exactly the inputs and outputs we want Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) - # Forward accumulate partial Jacobians + original_outputs = outputs total_Js = JacobianDict.identity(inputs) + + # horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! + vector_valued = set([k for k, v in ss.items() if np.size(v) > 1]) + inputs = (inputs | self._required) - vector_valued + outputs = (outputs | self._required) - vector_valued + + # Forward accumulate individual Jacobians + # ANNOYINGLY PARALLEL TO PARTIAL_JACOBIANS! for block in self.blocks: - if block.name in Js: - J = Js[block.name] - total_Js.update(J @ total_Js) + descendants = block.descendants if isinstance(block, Parent) else {block.name: None} + Js_block = {k: v for k, v in Js.items() if k in descendants} + J = block.jacobian(ss, inputs & block.inputs, outputs & block.outputs, T, Js_block) + total_Js.update(J @ total_Js) - return total_Js[outputs, :] + return total_Js[original_outputs, :] def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, sort_blocks=False, **kwargs): diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index d4185eb..118b8c1 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -86,9 +86,10 @@ def _jacobian(self, ss, inputs, outputs, T): else: J[o][i] = invertedJ[i][o] - print(J) - print(JacobianDict(J, name=self.name)) + if not isinstance(JacobianDict(J, name=self.name)[outputs, :], JacobianDict): + print('THIS IS THE WORST THING I HAVE EVER SEEN') + print(JacobianDict(J, name=self.name)[outputs, :]) return JacobianDict(J, name=self.name)[outputs, :] def compute_single_shock_J(self, ss, i): diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index 834dac8..67bee0b 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -1,3 +1,5 @@ +from ...utilities.ordered_set import OrderedSet + class Bijection: def __init__(self, map): # identity always implicit, remove if there explicitly @@ -42,6 +44,8 @@ def __matmul__(self, x): return {self[k] for k in x} elif isinstance(x, tuple): return tuple(self[k] for k in x) + elif isinstance(x, OrderedSet): + return OrderedSet([self[k] for k in x]) else: return NotImplemented diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index e56a277..1ec0203 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -7,6 +7,7 @@ from . import support from ..utilities.misc import factor, factored_solve +from ..utilities.ordered_set import OrderedSet from ..blocks.support.bijection import Bijection @@ -275,28 +276,27 @@ def __eq__(self, s): class NestedDict: - def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + def __init__(self, nesteddict, outputs: OrderedSet=None, inputs: OrderedSet=None, name: str=None): if isinstance(nesteddict, NestedDict): self.nesteddict = nesteddict.nesteddict - self.outputs = nesteddict.outputs - self.inputs = nesteddict.inputs - self.name = nesteddict.name + self.outputs: OrderedSet = nesteddict.outputs + self.inputs: OrderedSet = nesteddict.inputs + self.name: str = nesteddict.name else: self.nesteddict = nesteddict if outputs is None: - outputs = list(nesteddict.keys()) + outputs = OrderedSet(nesteddict.keys()) if inputs is None: - inputs = [] + inputs = OrderedSet([]) for v in nesteddict.values(): - inputs.extend(list(v)) - inputs = deduplicate(inputs) + inputs |= v if not outputs or not inputs: - outputs = [] - inputs = [] + outputs = OrderedSet([]) + inputs = OrderedSet([]) - self.outputs = list(outputs) - self.inputs = list(inputs) + self.outputs = OrderedSet(outputs) + self.inputs = OrderedSet(inputs) if name is None: # TODO: Figure out better default naming scheme for NestedDicts self.name = "NestedDict" @@ -346,13 +346,15 @@ def get(self, *args, **kwargs): return self.nesteddict.get(*args, **kwargs) def update(self, J): + if not J.outputs or not J.inputs: + return if set(self.inputs) != set(J.inputs): raise ValueError \ (f'Cannot merge {type(self).__name__}s with non-overlapping inputs {set(self.inputs) ^ set(J.inputs)}') if not set(self.outputs).isdisjoint(J.outputs): raise ValueError \ (f'Cannot merge {type(self).__name__}s with overlapping outputs {set(self.outputs) & set(J.outputs)}') - self.outputs = self.outputs + J.outputs + self.outputs = self.outputs | J.outputs self.nesteddict = {**self.nesteddict, **J.nesteddict} # Ensure that every output in self has either a Jacobian or filler value for each input, diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 4a04d75..dc08100 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -111,7 +111,7 @@ def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): # if you have a J for this block that already has everything you need, use it # TODO: add check for T, maybe look at verify_saved_jacobian for ideas? if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): - return Js[self.name][outputs, inputs] + return {self.name: Js[self.name][outputs, inputs]} # if it's a leaf, just call Jacobian method, include if nonzero if not isinstance(self, Parent): @@ -139,7 +139,9 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis return self._jacobian(ss, inputs, outputs, T) # otherwise remap own J (currently needed for SolvedBlock only) - Js = {**{k: v for k, v in Js.items() if k != self.name}, self.name: self.M.inv @ Js[self.name]} + Js = Js.copy() + if self.name in Js: + Js[self.name] = self.M.inv @ Js[self.name] return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], @@ -201,7 +203,7 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, inputs, T) from . import combine - self_with_unknowns = combine(U_Z, self) + self_with_unknowns = combine([U_Z, self]) return self_with_unknowns.jacobian(ss, inputs, outputs, T, Js) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py new file mode 100644 index 0000000..5fe0a2b --- /dev/null +++ b/src/sequence_jacobian/utilities/function.py @@ -0,0 +1,73 @@ +from sequence_jacobian.utilities.ordered_set import OrderedSet +import re +import inspect + +# TODO: fix this, have it twice (main version in misc) due to circular import problem +# let's make everything point to here for input_list, etc. so that this is unnecessary +def make_tuple(x): + """If not tuple or list, make into tuple with one element. + + Wrapping with this allows user to write, e.g.: + "return r" rather than "return (r,)" + "policy='a'" rather than "policy=('a',)" + """ + return (x,) if not (isinstance(x, tuple) or isinstance(x, list)) else x + + +def input_list(f): + """Return list of function inputs (both positional and keyword arguments)""" + return list(inspect.signature(f).parameters) + + +def input_arg_list(f): + """Return list of function positional arguments *only*""" + arg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default == p.empty: + arg_list.append(p.name) + return arg_list + + +def input_kwarg_list(f): + """Return list of function keyword arguments *only*""" + kwarg_list = [] + for p in inspect.signature(f).parameters.values(): + if p.default != p.empty: + kwarg_list.append(p.name) + return kwarg_list + + +def output_list(f): + """Scans source code of function to detect statement like + + 'return L, Div' + + and reports the list ['L', 'Div']. + + Important to write functions in this way when they will be scanned by output_list, for + either SimpleBlock or HetBlock. + """ + return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') + + +def metadata(f): + name = f.__name__ + inputs = OrderedSet(input_list(f)) + outputs = OrderedSet(output_list(f)) + return name, inputs, outputs + + +class ExtendedFunction: + """Wrapped function that knows its inputs and outputs. Evaluates on dict containing necessary + inputs, returns dict containing outputs by name""" + + def __init__(self, f): + self.f = f + self.name, self.inputs, self.outputs = metadata(f) + + def __call__(self, input_dict): + # take subdict of d contained in inputs + # this allows for d not to include all inputs (if there are optional inputs) + input_dict = {k: v for k, v in input_dict.items() if k in self.inputs} + return self.outputs.dict_from(make_tuple(self.f(**input_dict))) + diff --git a/src/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py index dbaf4b4..cc03ea8 100644 --- a/src/sequence_jacobian/utilities/misc.py +++ b/src/sequence_jacobian/utilities/misc.py @@ -2,8 +2,6 @@ import numpy as np import scipy.linalg -import re -import inspect def make_tuple(x): @@ -16,42 +14,6 @@ def make_tuple(x): return (x,) if not (isinstance(x, tuple) or isinstance(x, list)) else x -def input_list(f): - """Return list of function inputs (both positional and keyword arguments)""" - return list(inspect.signature(f).parameters) - - -def input_arg_list(f): - """Return list of function positional arguments *only*""" - arg_list = [] - for p in inspect.signature(f).parameters.values(): - if p.default == p.empty: - arg_list.append(p.name) - return arg_list - - -def input_kwarg_list(f): - """Return list of function keyword arguments *only*""" - kwarg_list = [] - for p in inspect.signature(f).parameters.values(): - if p.default != p.empty: - kwarg_list.append(p.name) - return kwarg_list - - -def output_list(f): - """Scans source code of function to detect statement like - - 'return L, Div' - - and reports the list ['L', 'Div']. - - Important to write functions in this way when they will be scanned by output_list, for - either SimpleBlock or HetBlock. - """ - return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') - - def numeric_primitive(instance): # If it is already a primitive, just return it if type(instance) in {int, float}: @@ -176,3 +138,6 @@ def logsum(vfun, lam): # apply formula (could be njitted in separate function) VE = vmax + lam * np.log(np.sum(np.exp(vfun_norm / lam), axis=0)) return VE + + +from .function import (input_list, input_arg_list, input_kwarg_list, output_list) diff --git a/src/sequence_jacobian/utilities/ordered_set.py b/src/sequence_jacobian/utilities/ordered_set.py new file mode 100644 index 0000000..6cee1bc --- /dev/null +++ b/src/sequence_jacobian/utilities/ordered_set.py @@ -0,0 +1,138 @@ +from typing import Iterable + +class OrderedSet: + """Ordered set implemented as dict (where key insertion order is preserved) mapping all to None. + + Operations on multiple ordered sets (e.g. union) order all members of first argument first, then + second argument. If a member is in both, order is as early as possible. + + See test_misc_support.test_ordered_set() for examples.""" + + def __init__(self, members: Iterable): + self.d = {k: None for k in members} + + def dict_from(self, s): + return dict(zip(self, s)) + + def __iter__(self): + return iter(self.d) + + def __repr__(self): + return f"OrderedSet({list(self)})" + + def __str__(self): + return str(list(self.d)) + + def __contains__(self, k): + return k in self.d + + def __len__(self): + return len(self.d) + + def add(self, x): + self.d[x] = None + + def difference(self, s): + return OrderedSet({k: None for k in self if k not in s}) + + def difference_update(self, s): + self.d = self.difference(s).d + return self + + def discard(self, k): + self.d.pop(k, None) + + def intersection(self, s): + return OrderedSet({k: None for k in self if k in s}) + + def intersection_update(self, s): + self.d = self.intersection(s).d + return self + + def isdisjoint(self, s): + return len(self.intersection(s)) == 0 + + def issubset(self, s): + return len(self.difference(s)) == 0 + + def issuperset(self, s): + return len(self.intersection(s)) == len(s) + + def remove(self, k): + self.d.pop(k) + + def symmetric_difference(self, s): + diff = self.difference(s) + for k in s: + if k not in self: + diff.add(k) + return diff + + def symmetric_difference_update(self, s): + self.d = self.symmetric_difference(s).d + return self + + def union(self, s): + return self.copy().update(s) + + def update(self, s): + for k in s: + self.add(k) + return self + + def copy(self): + return OrderedSet(self) + + def __eq__(self, s): + if isinstance(s, OrderedSet): + return list(self) == list(s) + else: + return False + + def __le__(self, s): + return self.issubset(s) + + def __lt__(self, s): + return self.issubset(s) and (len(self) != len(s)) + + def __ge__(self, s): + return self.issuperset(s) + + def __gt__(self, s): + return self.issuperset(s) and (len(self) != len(s)) + + def __or__(self, s): + return self.union(s) + + def __ior__(self, s): + return self.update(s) + + def __ror__(self, s): + return self.union(s) + + def __and__(self, s): + return self.intersection(s) + + def __iand__(self, s): + return self.intersection_update(s) + + def __rand__(self, s): + return self.intersection(s) + + def __sub__(self, s): + return self.difference(s) + + def __isub__(self, s): + return self.difference_update(s) + + def __rsub__(self, s): + return OrderedSet(s).difference(self) + + def __xor__(self, s): + return self.symmetric_difference(s) + + def __ixor__(self, s): + return self.symmetric_difference_update(s) + + def __rxor__(self, s): + return OrderedSet(s).symmetric_difference(self) diff --git a/tests/utils/test_function.py b/tests/utils/test_function.py new file mode 100644 index 0000000..d3c9a48 --- /dev/null +++ b/tests/utils/test_function.py @@ -0,0 +1,24 @@ +from sequence_jacobian.utilities.ordered_set import OrderedSet +from sequence_jacobian.utilities.function import ExtendedFunction, metadata + + +def f1(a, b, c): + k = a + 1 + l = b - c + return k, l + +def f2(b): + k = b + 4 + return k + + +def test_metadata(): + assert metadata(f1) == ('f1', OrderedSet(['a', 'b', 'c']), OrderedSet(['k', 'l'])) + assert metadata(f2) == ('f2', OrderedSet(['b']), OrderedSet(['k'])) + + +def test_extended_function(): + inputs = {'a': 1, 'b': 2, 'c': 3} + assert ExtendedFunction(f1)(inputs) == {'k': 2, 'l': -1} + assert ExtendedFunction(f2)(inputs) == {'k': 6} + diff --git a/tests/utils/test_ordered_set.py b/tests/utils/test_ordered_set.py new file mode 100644 index 0000000..1de3ea8 --- /dev/null +++ b/tests/utils/test_ordered_set.py @@ -0,0 +1,57 @@ +from sequence_jacobian.utilities.ordered_set import OrderedSet + +def test_ordered_set(): + # order matters + assert OrderedSet([1,2,3]) != OrderedSet([3,2,1]) + + # first insertion determines order + assert OrderedSet([5,1,6,5]) == OrderedSet([5,1,6]) + + # union preserves first and second order + assert (OrderedSet([6,1,3]) | OrderedSet([3,1,7,9])) == OrderedSet([6,1,3,7,9]) + + # intersection preserves first order + assert (OrderedSet([6,1,3]) & OrderedSet([3,1,7])) == OrderedSet([1,3]) + + # difference works + assert (OrderedSet([6,1,3,2]) - OrderedSet([3,1,7])) == OrderedSet([6,2]) + + # symmetric difference: first then second + assert (OrderedSet([6,1,3,8]) ^ OrderedSet([3,1,7,9])) == OrderedSet([6,8,7,9]) + + # in-place versions of these + s = OrderedSet([6,1,3]) + s2 = s + s2 |= OrderedSet([3,1,7,9]) + assert s == OrderedSet([6,1,3,7,9]) + + s = OrderedSet([6,1,3]) + s2 = s + s2 &= OrderedSet([3,1,7]) + assert s == OrderedSet([1,3]) + + s = OrderedSet([6,1,3,2]) + s2 = s + s2 -= OrderedSet([3,1,7]) + assert s == OrderedSet([6,2]) + + s = OrderedSet([6,1,3,8]) + s2 = s + s2 ^= OrderedSet([3,1,7,9]) + assert s == OrderedSet([6,8,7,9]) + + # comparisons (order not used for these) + assert OrderedSet([4,3,2,1]) <= OrderedSet([1,2,3,4]) + assert not (OrderedSet([4,3,2,1]) < OrderedSet([1,2,3,4])) + assert OrderedSet([3,2,1]) < OrderedSet([1,2,3,4]) + + # allow second argument (but ONLY second argument) to be any iterable, not just ordered set + # we use the order from the iterable... + assert (OrderedSet([6,1,3]) | [3,1,7,9]) == OrderedSet([6,1,3,7,9]) + assert (OrderedSet([6,1,3]) & [3,1,7]) == OrderedSet([1,3]) + assert (OrderedSet([6,1,3,2]) - [3,1,7]) == OrderedSet([6,2]) + assert (OrderedSet([6,1,3,8]) ^ [3,1,7,9]) == OrderedSet([6,8,7,9]) + + +def test_ordered_set_dict_from(): + assert OrderedSet(['a','b','c']).dict_from([1, 2, 3]) == {'a': 1, 'b': 2, 'c': 3} \ No newline at end of file From 5cdc61a635cfc9412850ce7668e6122d633d227f Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Mon, 16 Aug 2021 15:26:32 -0500 Subject: [PATCH 215/288] minor refactor of SimpleBlock to use ExtendedFunction; impulse methods not changed yet, and for some reason cannot use OrderedSet for block inputs or outputs --- src/sequence_jacobian/blocks/simple_block.py | 28 +++++++++----------- src/sequence_jacobian/utilities/function.py | 11 ++++++++ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 118b8c1..a5b826d 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -11,6 +11,7 @@ from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix, verify_saved_jacobian from ..utilities import misc +from ..utilities.function import ExtendedFunction '''Part 1: SimpleBlock class and @simple decorator to generate it''' @@ -36,20 +37,18 @@ def production(Z, K, L, alpha): def __init__(self, f): super().__init__() - self.f = f - self.name = f.__name__ - self.output_list = misc.output_list(f) - self.inputs = set(misc.input_list(f)) - self.outputs = set(self.output_list) + self.f = ExtendedFunction(f) + self.name = self.f.name + # TODO: if we do OrderedSet here instead of set, things break! + self.inputs = set(self.f.inputs) + self.outputs = set(self.f.outputs) def __repr__(self): return f"" - def _steady_state(self, calibration): - input_args = {k: ignore(v) for k, v in calibration.items() if k in misc.input_list(self.f)} - output_vars = [misc.numeric_primitive(o) for o in self.f(**input_args)] if len(self.output_list) > 1 else [ - misc.numeric_primitive(self.f(**input_args))] - return SteadyStateDict({**calibration, **dict(zip(self.output_list, output_vars))}) + def _steady_state(self, ss): + outputs = self.f.wrapped_call(ss, preprocess=ignore, postprocess=misc.numeric_primitive) + return SteadyStateDict({**ss, **outputs}) def _impulse_nonlinear(self, ss, exogenous): input_args = {} @@ -86,18 +85,15 @@ def _jacobian(self, ss, inputs, outputs, T): else: J[o][i] = invertedJ[i][o] - - if not isinstance(JacobianDict(J, name=self.name)[outputs, :], JacobianDict): - print('THIS IS THE WORST THING I HAVE EVER SEEN') - print(JacobianDict(J, name=self.name)[outputs, :]) + to_return = JacobianDict(J, name=self.name)[outputs, :] return JacobianDict(J, name=self.name)[outputs, :] def compute_single_shock_J(self, ss, i): input_args = {i: ignore(ss[i]) for i in self.inputs} input_args[i] = AccumulatedDerivative(f_value=ss[i]) - J = {o: {} for o in self.output_list} - for o, o_name in zip(misc.make_tuple(self.f(**input_args)), self.output_list): + J = {o: {} for o in self.outputs} + for o_name, o in self.f(input_args).items(): if isinstance(o, AccumulatedDerivative): J[o_name] = SimpleSparse(o.elements) diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 5fe0a2b..9df1696 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -71,3 +71,14 @@ def __call__(self, input_dict): input_dict = {k: v for k, v in input_dict.items() if k in self.inputs} return self.outputs.dict_from(make_tuple(self.f(**input_dict))) + def wrapped_call(self, input_dict, preprocess=None, postprocess=None): + if preprocess is not None: + input_dict = {k: preprocess(v) for k, v in input_dict.items() if k in self.inputs} + else: + input_dict = {k: v for k, v in input_dict.items() if k in self.inputs} + + output_dict = self.outputs.dict_from(make_tuple(self.f(**input_dict))) + if postprocess is not None: + output_dict = {k: postprocess(v) for k, v in output_dict.items()} + + return output_dict From 1334e8f66a0c57c3a9cac530eeed98be07133d86 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 17 Aug 2021 13:11:00 -0500 Subject: [PATCH 216/288] tracked down bug giving inconsistent results: JacobianDict was not keeping track of order of inputs and outputs, which we made into sets, so packing was in irregular orders. Now using OrderedSet --- src/sequence_jacobian/blocks/simple_block.py | 6 ++++-- src/sequence_jacobian/jacobian/classes.py | 2 +- src/sequence_jacobian/primitives.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index a5b826d..10dfdf1 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -40,8 +40,10 @@ def __init__(self, f): self.f = ExtendedFunction(f) self.name = self.f.name # TODO: if we do OrderedSet here instead of set, things break! - self.inputs = set(self.f.inputs) - self.outputs = set(self.f.outputs) + #self.inputs = set(self.f.inputs) + #self.outputs = set(self.f.outputs) + self.inputs = self.f.inputs + self.outputs = self.f.outputs def __repr__(self): return f"" diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index 1ec0203..6248ad5 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -335,7 +335,7 @@ def __getitem__(self, x): # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i i = (i,) if isinstance(i, str) else i return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) - elif isinstance(x, list) or isinstance(x, set): + elif isinstance(x, OrderedSet) or isinstance(x, list) or isinstance(x, set): # case 3: assume that list or set refers just to outputs, get all of those return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) else: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index dc08100..e880134 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -7,6 +7,8 @@ from typing import Any, Dict, Union, Tuple, Optional, List from copy import deepcopy +from sequence_jacobian.utilities.ordered_set import OrderedSet + from .steady_state.drivers import steady_state as ss from .steady_state.support import provide_solver_default from .nonlinear import td_solve @@ -128,7 +130,7 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis T: Optional[int] = None, Js={}) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" inputs, outputs = self.default_inputs_outputs(inputs, outputs) - inputs, outputs = set(inputs), set(outputs) + inputs, outputs = OrderedSet(inputs), OrderedSet(outputs) # if you have a J for this block that has everything you need, use it if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): From 2eca1d646192c49f1ed939ee914537ea03cdb342 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 17 Aug 2021 16:11:45 -0500 Subject: [PATCH 217/288] all tests pass now except two in test_transitional_dynamics, which fail because HetOutputs are treated as top-level outputs and then the default jacobian asks for them, and HetBlock cannot provide that now. --- .../blocks/combined_block.py | 4 +- src/sequence_jacobian/blocks/simple_block.py | 18 +++---- src/sequence_jacobian/blocks/solved_block.py | 2 +- src/sequence_jacobian/jacobian/drivers.py | 10 ++-- src/sequence_jacobian/nonlinear.py | 2 +- src/sequence_jacobian/primitives.py | 19 ++++---- src/sequence_jacobian/steady_state/classes.py | 6 +++ tests/base/test_estimation.py | 2 +- tests/base/test_jacobian.py | 47 ++++++++++--------- tests/base/test_jacobian_dict_block.py | 7 +-- tests/base/test_public_classes.py | 45 ------------------ tests/base/test_simple_block.py | 4 +- tests/base/test_solved_block.py | 8 ++-- tests/base/test_transitional_dynamics.py | 18 +++---- 14 files changed, 76 insertions(+), 116 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index faf3456..54a16b9 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -105,7 +105,7 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): def _partial_jacobians(self, ss, inputs, outputs, T, Js): """Calculate partial Jacobians (i.e. without forward accumulation) wrt `inputs` and outputs of other blocks.""" # Add intermediate inputs; remove vector-valued inputs - vector_valued = set([k for k, v in ss.items() if np.size(v) > 1]) + vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued @@ -130,7 +130,7 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): total_Js = JacobianDict.identity(inputs) # horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! - vector_valued = set([k for k, v in ss.items() if np.size(v) > 1]) + vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 10dfdf1..32846d1 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -59,11 +59,11 @@ def _impulse_nonlinear(self, ss, exogenous): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) - for k in self.input_list: + for k in self.inputs: if k not in input_args: input_args[k] = ignore(ss[k]) - return ImpulseDict(make_impulse_uniform_length(self.f(**input_args), self.output_list)) - ss + return ImpulseDict(make_impulse_uniform_length(self.f(input_args))) - ss def _impulse_linear(self, ss, exogenous, T=None, Js=None): return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) @@ -102,13 +102,7 @@ def compute_single_shock_J(self, ss, i): return J -def make_impulse_uniform_length(out, output_list): - # If the function has multiple outputs - if isinstance(out, tuple): - # Because we know at least one of the outputs in `out` must be of length T - T = np.max([np.size(o) for o in out]) - out_unif_dim = [np.full(T, misc.numeric_primitive(o)) if np.isscalar(o) else - misc.numeric_primitive(o) for o in out] - return dict(zip(output_list, misc.make_tuple(out_unif_dim))) - else: - return dict(zip(output_list, misc.make_tuple(misc.numeric_primitive(out)))) +def make_impulse_uniform_length(out): + T = np.max([np.size(v) for v in out.values()]) + return {k: (np.full(T, misc.numeric_primitive(v)) if np.isscalar(v) else misc.numeric_primitive(v)) + for k, v in out.items()} diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index ce7d41f..19d5edd 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -83,7 +83,7 @@ def _impulse_linear(self, ss, exogenous, T=None, Js=None): T=T, Js=Js) def _jacobian(self, ss, inputs, outputs, T, Js): - return self.block.solve_jacobian(ss, set(self.unknowns), set(self.targets), inputs, outputs, T, Js) + return self.block.solve_jacobian(ss, set(self.unknowns), set(self.targets), inputs, outputs, T, Js)[outputs] def _partial_jacobians(self, ss, inputs, outputs, T, Js={}): # call it on the child first diff --git a/src/sequence_jacobian/jacobian/drivers.py b/src/sequence_jacobian/jacobian/drivers.py index 16131f9..afc0416 100644 --- a/src/sequence_jacobian/jacobian/drivers.py +++ b/src/sequence_jacobian/jacobian/drivers.py @@ -16,7 +16,7 @@ ''' -def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): +def get_H_U(blocks, unknowns, targets, T, ss=None, Js={}): """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. Parameters @@ -48,7 +48,7 @@ def get_H_U(blocks, unknowns, targets, T, ss=None, Js=None): return H_U_unpacked[targets, unknowns].pack(T) -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js=None): +def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js={}): """Get a single general equilibrium impulse response. Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed @@ -102,7 +102,7 @@ def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js=None): +def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js={}): """Compute Jacobians G that fully characterize general equilibrium outputs in response to all exogenous shocks in 'exogenous' @@ -149,7 +149,7 @@ def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) -def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): +def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js={}): """ Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs and with respect to outputs of other blocks @@ -183,7 +183,7 @@ def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js=None): shocks = set(inputs) | required for num in topsorted: block = blocks[num] - jac = block.jacobian(ss, exogenous=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() + jac = block.jacobian(ss, inputs=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() if k in misc.input_kwarg_list(block.jacobian)}) # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) diff --git a/src/sequence_jacobian/nonlinear.py b/src/sequence_jacobian/nonlinear.py index e204b56..3661e6e 100644 --- a/src/sequence_jacobian/nonlinear.py +++ b/src/sequence_jacobian/nonlinear.py @@ -7,7 +7,7 @@ from .jacobian.support import pack_vectors, unpack_vectors -def td_solve(block_list, ss, exogenous, unknowns, targets, Js=None, monotonic=False, +def td_solve(block_list, ss, exogenous, unknowns, targets, Js={}, monotonic=False, returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index e880134..df46a90 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -129,7 +129,7 @@ def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, Js={}) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" - inputs, outputs = self.default_inputs_outputs(inputs, outputs) + inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) inputs, outputs = OrderedSet(inputs), OrderedSet(outputs) # if you have a J for this block that has everything you need, use it @@ -138,6 +138,7 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis # if it's a leaf, call Jacobian method, don't supply Js if not isinstance(self, Parent): + # TODO should this be remapped? return self._jacobian(ss, inputs, outputs, T) # otherwise remap own J (currently needed for SolvedBlock only) @@ -160,7 +161,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], - Js: Optional[Dict[str, JacobianDict]] = None, + Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous @@ -175,7 +176,7 @@ def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], exogenous: Dict[str, Array], unknowns: List[str], targets: List[str], T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = None, + Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous @@ -195,10 +196,10 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], if T is None: T = 300 - inputs, outputs = self.default_inputs_outputs(inputs, outputs) + inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) inputs, unknowns, targets = list(inputs), list(unknowns), list(targets) - Js = self.partial_jacobians(ss, set(inputs) | set(unknowns), set(outputs) | set(targets), T, Js) + Js = self.partial_jacobians(ss, set(inputs) | set(unknowns), (set(outputs) | set(targets)) - set(unknowns), T, Js) H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) @@ -206,7 +207,7 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], from . import combine self_with_unknowns = combine([U_Z, self]) - return self_with_unknowns.jacobian(ss, inputs, outputs, T, Js) + return self_with_unknowns.jacobian(ss, inputs, set(unknowns) | set(outputs), T, Js) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: @@ -233,9 +234,11 @@ def rename(self, name): renamed.name = name return renamed - def default_inputs_outputs(self, inputs, outputs): + def default_inputs_outputs(self, ss: SteadyStateDict, inputs, outputs): + # TODO: there should be checks to make sure you don't ask for multidimensional stuff for Jacobians? + # should you be allowed to ask for it (even if not default) for impulses? if inputs is None: inputs = self.inputs if outputs is None: - outputs = self.outputs + outputs = self.outputs - ss._vector_valued() return inputs, outputs diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index 63df6db..b2dd228 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -3,8 +3,11 @@ from copy import deepcopy from ..utilities.misc import dict_diff +from ..utilities.ordered_set import OrderedSet from ..blocks.support.bijection import Bijection +import numpy as np + from numbers import Real from typing import Any, Dict, Union Array = Any @@ -78,4 +81,7 @@ def update(self, ssdict): def difference(self, data_to_remove): return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internal)) + def _vector_valued(self): + return OrderedSet([k for k, v in self.toplevel.items() if np.size(v) > 1]) + UserProvidedSS = Dict[str, Union[Real, Array]] diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 97a2903..e485c49 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -13,7 +13,7 @@ def test_krusell_smith_estimation(krusell_smith_dag): np.random.seed(41234) T = 50 - G = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + G = ks_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) # Step 1: Stacked impulse responses rho = 0.9 diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 6bd4a1d..24c2714 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -12,11 +12,11 @@ def test_ks_jac(krusell_smith_dag): T = 10 # Automatically calculate the general equilibrium Jacobian - G2 = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + G2 = ks_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) # Manually calculate the general equilibrium Jacobian - J_firm = firm.jacobian(ss, exogenous=['K', 'Z']) - J_ha = household.jacobian(ss, T=T, exogenous=['r', 'w']) + J_firm = firm.jacobian(ss, inputs=['K', 'Z']) + J_ha = household.jacobian(ss, T=T, inputs=['r', 'w']) J_curlyK_K = J_ha['A']['r'] @ J_firm['r']['K'] + J_ha['A']['w'] @ J_firm['w']['K'] J_curlyK_Z = J_ha['A']['r'] @ J_firm['r']['Z'] + J_ha['A']['w'] @ J_firm['w']['Z'] J_curlyK = {'curlyK': {'K': J_curlyK_K, 'Z': J_curlyK_Z}} @@ -34,27 +34,28 @@ def test_ks_jac(krusell_smith_dag): assert np.allclose(G2[o]['Z'], G[o]) -def test_hank_jac(one_asset_hank_dag): - hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag - T = 10 +# TODO: decide whether to get rid of this or revise it with manual solve_jacobian stuff +# def test_hank_jac(one_asset_hank_dag): +# hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag +# T = 10 - # Automatically calculate the general equilibrium Jacobian - G2 = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) +# # Automatically calculate the general equilibrium Jacobian +# G2 = hank_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) - # Manually calculate the general equilibrium Jacobian - curlyJs, required = curlyJ_sorted(hank_model.blocks, unknowns + exogenous, ss, T) - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) - J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) - H_U = J_curlyH_U[targets, unknowns].pack(T) - H_Z = J_curlyH_Z[targets, exogenous].pack(T) - G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) - curlyJs = [G_U] + curlyJs - outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - G = forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) +# # Manually calculate the general equilibrium Jacobian +# curlyJs, required = curlyJ_sorted(hank_model.blocks, unknowns + exogenous, ss, T) +# J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) +# J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) +# H_U = J_curlyH_U[targets, unknowns].pack(T) +# H_Z = J_curlyH_Z[targets, exogenous].pack(T) +# G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) +# curlyJs = [G_U] + curlyJs +# outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) +# G = forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) - for o in G: - for i in G[o]: - assert np.allclose(G[o][i], G2[o][i]) +# for o in G: +# for i in G[o]: +# assert np.allclose(G[o][i], G2[o][i]) def test_fake_news_v_actual(one_asset_hank_dag): @@ -63,7 +64,7 @@ def test_fake_news_v_actual(one_asset_hank_dag): household = hank_model._blocks_unsorted[0] T = 40 exogenous = ['w', 'r', 'Div', 'Tax'] - Js = household.jacobian(ss, exogenous, T) + Js = household.jacobian(ss, exogenous, T=T) output_list = household.non_back_iter_outputs # Preliminary processing of the steady state @@ -130,7 +131,7 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): output_list = household.non_back_iter_outputs h = 1E-4 - Js = household.jacobian(ss, exogenous, T) + Js = household.jacobian(ss, exogenous, T=T) Js_direct = {o.capitalize(): {i: np.empty((T, T)) for i in exogenous} for o in output_list} # run td once without any shocks to get paths to subtract against diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 1d59376..3f38b78 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -5,16 +5,17 @@ from sequence_jacobian import combine from sequence_jacobian.models import rbc from sequence_jacobian.blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock +from sequence_jacobian.steady_state.classes import SteadyStateDict def test_jacobian_dict_block_impulses(rbc_dag): rbc_model, exogenous, unknowns, _, ss = rbc_dag T = 10 - J_pe = rbc_model.jacobian(ss, exogenous=unknowns + exogenous, T=10) + J_pe = rbc_model.jacobian(ss, inputs=unknowns + exogenous, T=10) J_block = JacobianDictBlock(J_pe) - J_block_Z = J_block.jacobian(["Z"]) + J_block_Z = J_block.jacobian(SteadyStateDict({}), ["Z"]) for o in J_block_Z.outputs: assert np.all(J_block[o]["Z"] == J_block_Z[o]["Z"]) @@ -30,7 +31,7 @@ def test_jacobian_dict_block_impulses(rbc_dag): def test_jacobian_dict_block_combine(rbc_dag): _, exogenous, _, _, ss = rbc_dag - J_firm = rbc.firm.jacobian(ss, exogenous=exogenous) + J_firm = rbc.firm.jacobian(ss, inputs=exogenous) blocks_w_jdict = [rbc.household, J_firm, rbc.mkt_clearing] cblock_w_jdict = combine(blocks_w_jdict) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index ceee3d3..f40aa1a 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -9,51 +9,6 @@ from sequence_jacobian.blocks.support.bijection import Bijection -def test_steadystatedict(): - toplevel = {"A": 1., "B": 2.} - internal = {"block1": {"a": np.array([0, 1]), "D": np.array([[0, 0.5], [0.5, 0]]), - "Pi": np.array([[0.5, 0.5], [0.5, 0.5]])}} - raw_output = {"A": 1., "B": 2., "a": np.array([0, 1]), "D": np.array([[0, 0.5], [0.5, 0]]), - "Pi": np.array([[0.5, 0.5], [0.5, 0.5]])} - - @het(exogenous="Pi", policy="a", backward="Va") - def block1(Va_p, Pi_p, t1, t2): - Va = Va_p - a = t1 + t2 - return Va, a - - ss1 = SteadyStateDict(toplevel, internal=internal) - ss2 = SteadyStateDict(raw_output, internal=block1) - - # Test that both ways of instantiating SteadyStateDict given by ss1 and ss2 are equivalent - def check_steady_states(ss1, ss2): - assert set(ss1.keys()) == set(ss2.keys()) - for k in ss1: - assert np.isclose(ss1[k], ss2[k]) - - assert set(ss1.internal.keys()) == set(ss2.internal.keys()) - for k in ss1.internal: - assert set(ss1.internal[k].keys()) == set(ss2.internal[k].keys()) - for kk in ss1.internal[k]: - assert np.all(np.isclose(ss1.internal[k][kk], ss2.internal[k][kk])) - - check_steady_states(ss1, ss2) - - # Test iterable indexing - assert ss1[["A", "B"]] == {"A": 1., "B": 2.} - - # Test updating - toplevel_new = {"C": 2., "D": 4.} - internal_new = {"block1_new": {"a": np.array([2, 0]), "D": np.array([[0.25, 0.25], [0.25, 0.25]]), - "Pi": np.array([[0.2, 0.8], [0.8, 0.2]])}} - ss_new = SteadyStateDict(toplevel_new, internal=internal_new) - - ss1.update(ss_new) - ss2.update(toplevel_new, internal_namespaces=internal_new) - - check_steady_states(ss1, ss2) - - def test_impulsedict(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 200 diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 9c6fda4..721f969 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -46,14 +46,14 @@ def test_block_consistency(block, ss): assert np.all(v == 0) # now get the Jacobian - J = block.jacobian(ss, exogenous=block.input_list) + J = block.jacobian(ss, inputs=block.inputs) # now perturb the steady state by small random vectors # and verify that the second-order numerical derivative implied by .td # is equivalent to what we get from jac h = 1E-5 - all_shocks = {i: np.random.rand(10) for i in block.input_list} + all_shocks = {i: np.random.rand(10) for i in block.inputs} td_up = block.impulse_nonlinear(ss_results, exogenous={i: h*shock for i, shock in all_shocks.items()}) td_dn = block.impulse_nonlinear(ss_results, exogenous={i: -h*shock for i, shock in all_shocks.items()}) diff --git a/tests/base/test_solved_block.py b/tests/base/test_solved_block.py index 60fb2ea..1c2eb7c 100644 --- a/tests/base/test_solved_block.py +++ b/tests/base/test_solved_block.py @@ -19,12 +19,12 @@ def myblock_solved(u, i): ss = SteadyStateDict({'u': 5, 'i': 10, 'res': 0.0}) # Compute jacobian of myblock_solved from scratch -J1 = myblock_solved.jacobian(ss, exogenous=['i'], T=20) +J1 = myblock_solved.jacobian(ss, inputs=['i'], T=20) # Compute jacobian of SolvedBlock using a pre-computed FactoredJacobian -J_u = myblock.jacobian(ss, exogenous=['u'], T=20) # square jac of underlying simple block +J_u = myblock.jacobian(ss, inputs=['u'], T=20) # square jac of underlying simple block J_factored = FactoredJacobianDict(J_u, T=20) -J_i = myblock.jacobian(ss, exogenous=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns -J2 = J_factored.compose(J_i, T=20) # obtain jac of unknown wrt to non-unknown inputs using factored jac +J_i = myblock.jacobian(ss, inputs=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns +J2 = J_factored.compose(J_i, T=20) # obtain jac of unknown wrt to non-unknown inputs using factored jac assert np.allclose(J1['u']['i'], J2['u']['i']) \ No newline at end of file diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 8c9fc94..72f332a 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -12,7 +12,7 @@ def test_rbc_td(rbc_dag): rbc_model, exogenous, unknowns, targets, ss = rbc_dag T, impact, rho, news = 30, 0.01, 0.8, 10 - G = rbc_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + G = rbc_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) dZ = np.empty((T, 2)) dZ[:, 0] = impact * ss['Z'] * rho**np.arange(T) @@ -33,7 +33,7 @@ def test_ks_td(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag T = 30 - G = ks_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T) + G = ks_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) for shock_size, tol in [(0.01, 7e-3), (0.1, 0.6)]: dZ = shock_size * 0.8 ** np.arange(T) @@ -51,8 +51,8 @@ def test_hank_td(one_asset_hank_dag): T = 30 household = hank_model._blocks_unsorted[0] - J_ha = household.jacobian(ss=ss, T=T, exogenous=['Div', 'Tax', 'r', 'w']) - G = hank_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) + J_ha = household.jacobian(ss=ss, T=T, inputs=['Div', 'Tax', 'r', 'w']) + G = hank_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) @@ -70,8 +70,8 @@ def test_two_asset_td(two_asset_hank_dag): T = 30 household = two_asset_model._blocks_unsorted[0] - J_ha = household.jacobian(ss=ss, T=T, exogenous=['N', 'r', 'ra', 'rb', 'tax', 'w']) - G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) + J_ha = household.jacobian(ss=ss, T=T, inputs=['N', 'r', 'ra', 'rb', 'tax', 'w']) + G = two_asset_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: drstar = shock_size * -0.0025 * 0.6 ** np.arange(T) @@ -98,9 +98,9 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): T = 30 household = two_asset_model._blocks_unsorted[0] - J_ha = household.jacobian(ss=ss, T=T, exogenous=['N', 'r', 'ra', 'rb', 'tax', 'w']) - G = two_asset_model.solve_jacobian(ss, exogenous, unknowns, targets, T=T, Js={'household': J_ha}) - G_simple = two_asset_model_simple.solve_jacobian(ss, exogenous, unknowns_simple, targets_simple, T=T, + J_ha = household.jacobian(ss=ss, T=T, inputs=['N', 'r', 'ra', 'rb', 'tax', 'w']) + G = two_asset_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) + G_simple = two_asset_model_simple.solve_jacobian(ss, unknowns_simple, targets_simple, exogenous, T=T, Js={'household': J_ha}) drstar = -0.0025 * 0.6 ** np.arange(T) From 329c6a4a2b6fd3f792e8a8d6c9e946590bc5b7a5 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 17 Aug 2021 16:35:35 -0500 Subject: [PATCH 218/288] by default SteadyStateDict now copies its toplevel and internal dictionaries both when constructed and with a .copy() method. Should render many deepcopy uses redundant. --- src/sequence_jacobian/steady_state/classes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index b2dd228..52a4ddb 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -20,11 +20,11 @@ def __init__(self, data, internal=None): if isinstance(data, SteadyStateDict): if internal is not None: raise ValueError('Supplying SteadyStateDict and also internal to constructor not allowed') - self.toplevel = data - self.internal = {} + self.toplevel = data.toplevel.copy() + self.internal = data.internal.copy() - self.toplevel: dict = data - self.internal: dict = {} if internal is None else internal + self.toplevel: dict = data.copy() + self.internal: dict = {} if internal is None else internal.copy() def __repr__(self): if self.internal: @@ -81,6 +81,9 @@ def update(self, ssdict): def difference(self, data_to_remove): return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internal)) + def copy(self): + return SteadyStateDict(self) + def _vector_valued(self): return OrderedSet([k for k, v in self.toplevel.items() if np.size(v) > 1]) From da1a4125b87d074965527f784f2ba837cbe91aef Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 19 Aug 2021 09:37:28 -0500 Subject: [PATCH 219/288] Fix bug with calling SteadyStateDict constructor on SteadyStateDicts resulting in infinite recursive copying --- src/sequence_jacobian/steady_state/classes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/steady_state/classes.py index 52a4ddb..8bb5f70 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/steady_state/classes.py @@ -22,9 +22,9 @@ def __init__(self, data, internal=None): raise ValueError('Supplying SteadyStateDict and also internal to constructor not allowed') self.toplevel = data.toplevel.copy() self.internal = data.internal.copy() - - self.toplevel: dict = data.copy() - self.internal: dict = {} if internal is None else internal.copy() + else: + self.toplevel: dict = data.copy() + self.internal: dict = {} if internal is None else internal.copy() def __repr__(self): if self.internal: From 1ebe03b2778a5dd246ad1ede429bab9b2619dd93 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 19 Aug 2021 12:13:20 -0500 Subject: [PATCH 220/288] preliminary work on ImpulseDict and JacobianDict, aiming toward impulse refactor --- .../blocks/combined_block.py | 2 +- src/sequence_jacobian/blocks/het_block.py | 6 +- .../blocks/support/impulse.py | 25 +++++++- src/sequence_jacobian/jacobian/classes.py | 62 +++++++++++++++---- src/sequence_jacobian/primitives.py | 3 +- 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 54a16b9..c447b4b 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -127,7 +127,7 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) original_outputs = outputs - total_Js = JacobianDict.identity(inputs) + total_Js = JacobianDict.identity(inputs, T=T) # horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! vector_valued = ss._vector_valued() diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 50689f6..6d82c2b 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -382,10 +382,6 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): J[o][i] for output o and input i gives T*T Jacobian of o with respect to i """ outputs = self.M_outputs.inv @ outputs # horrible - print('HERE ARE THE OUTPUTS') - print(outputs) - - print(self.M_outputs) # TODO: this is one instance of us letting people supply inputs that aren't actually inputs # This behavior should lead to an error instead (probably should be handled at top level) @@ -421,7 +417,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) - return JacobianDict(J, name=self.name) + return JacobianDict(J, name=self.name, T=T) def add_hetinput(self, hetinput, overwrite=False, verbose=True): # TODO: serious violation, this is mutating the block diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index 345363f..ecea31f 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -8,19 +8,33 @@ class ImpulseDict: - def __init__(self, impulse): + def __init__(self, impulse, T=None): if isinstance(impulse, ImpulseDict): self.impulse = impulse.impulse + self.T = impulse.T else: if not isinstance(impulse, dict): raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses.') self.impulse = impulse + if T is None: + T = self.infer_length() + self.T = T + + def keys(self): + return self.impulse.keys() + + def pack(self): + T = self.T + bigv = np.empty(T*len(self.impulse)) + for i, v in enumerate(self.impulse.values()): + bigv[i*T:(i+1)*T] = v + return bigv def __repr__(self): return f'' def __iter__(self): - return iter(self.impulse.items()) + return iter(self.impulse) def __or__(self, other): if not isinstance(other, ImpulseDict): @@ -93,3 +107,10 @@ def __matmul__(self, x): def __rmatmul__(self, x): return self.__matmul__(x) + + def infer_length(self): + lengths = [len(v) for v in self.impulse.values()] + length = max(lengths) + if lengths != min(lengths): + raise ValueError(f'Building ImpulseDict with inconsistent lengths {max(lengths)} and {min(lengths)}') + return length diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index 6248ad5..bda9153 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -5,10 +5,14 @@ import warnings import numpy as np +from sequence_jacobian.primitives import Array + from . import support from ..utilities.misc import factor, factored_solve from ..utilities.ordered_set import OrderedSet from ..blocks.support.bijection import Bijection +from ..blocks.support.impulse import ImpulseDict +from typing import Dict, Union class Jacobian(metaclass=ABCMeta): @@ -375,15 +379,17 @@ def deduplicate(mylist): class JacobianDict(NestedDict): - def __init__(self, nesteddict, outputs=None, inputs=None, name=None): + def __init__(self, nesteddict, outputs=None, inputs=None, name=None, T=None): ensure_valid_jacobiandict(nesteddict) super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) + self.T = T @staticmethod def identity(ks): return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() def complete(self): + # TODO: think about when and whether we want to use this return super().complete(ZeroMatrix()) def addinputs(self): @@ -410,6 +416,9 @@ def __bool__(self): return bool(self.outputs) and bool(self.inputs) def compose(self, J): + if self.T is not None and J.T is not None and self.T != J.T: + raise ValueError(f'Trying to multiply JacobianDicts with inconsistent dimensions {self.T} and {J.T}') + o_list = self.outputs m_list = tuple(set(self.inputs) & set(J.outputs)) i_list = J.inputs @@ -430,22 +439,30 @@ def compose(self, J): return JacobianDict(J_oi, o_list, i_list) - def apply(self, x): - # assume that all entries in x have some length T, and infer it - T = len(next(iter(x.values()))) + def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): + x = ImpulseDict(x) inputs = x.keys() & set(self.inputs) J_oi = self.complete().nesteddict y = {} for o in self.outputs: - y[o] = np.zeros(T) + y[o] = np.zeros(x.T) for i in inputs: y[o] += J_oi[o][i] @ x[i] - return y + return ImpulseDict(y, T=x.T) + + def pack(self, T=None): + if T is None: + if self.T is not None: + T = self.T + else: + raise ValueError('Trying to pack {self} into matrix, but do not know {T}') + else: + if self.T is not None and T != self.T: + raise ValueError('{self} has dimension {self.T}, but trying to pack it with alternate dimension {T}') - def pack(self, T): J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) for iO, O in enumerate(self.outputs): for iI, I in enumerate(self.inputs): @@ -460,10 +477,17 @@ def unpack(bigjac, outputs, inputs, T): jacdict[O] = {} for iI, I in enumerate(inputs): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] - return JacobianDict(jacdict, outputs, inputs) + return JacobianDict(jacdict, outputs, inputs, T=T) class FactoredJacobianDict(JacobianDict): - def __init__(self, jacobian_dict: JacobianDict, T): + def __init__(self, jacobian_dict: JacobianDict, T=None): + if jacobian_dict.T is None: + if T is None: + raise ValueError(f'Trying to factor (solve) {jacobian_dict} but do not know T') + self.T = T + else: + self.T = jacobian_dict.T + H_U = jacobian_dict.pack(T) self.targets = jacobian_dict.outputs self.unknowns = jacobian_dict.inputs @@ -475,14 +499,26 @@ def __init__(self, jacobian_dict: JacobianDict, T): def __repr__(self): return f'<{type(self).__name__} unknowns={self.unknowns}, targets={self.targets}>' - def compose(self, J: JacobianDict, T): + def compose(self, J: JacobianDict): # take intersection of J outputs with self.targets # then pack that reduced J into a matrix, then apply lu_solve, then unpack - Jsub = J[[o for o in self.targets if o in J.outputs]].pack(T) + Jsub = J[[o for o in self.targets if o in J.outputs]].pack(self.T) X = -factored_solve(self.H_U_factored, Jsub) - return JacobianDict.unpack(X, self.unknowns, J.inputs, T) + return JacobianDict.unpack(X, self.unknowns, J.inputs, self.T) + + def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): + x = ImpulseDict(x)[self.targets] + + inputs = x.keys() & set(self.targets) + #J_oi = self.complete().nesteddict + # y = {} + + # for o in self.outputs: + # y[o] = np.zeros(x.T) + # for i in inputs: + # y[o] += J_oi[o][i] @ x[i] - def apply(self, x): + # return ImpulseDict(y, T=x.T) # take intersection of x entries with self.targets # then pack (should be ImpulseDict?) into a vector, then apply lu_solve, then unpack pass diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index df46a90..0cb4168 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -96,8 +96,7 @@ def impulse_nonlinear(self, ss: SteadyStateDict, from a steady state `ss`.""" return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def impulse_linear(self, ss: SteadyStateDict, - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: + def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js={}) -> ImpulseDict: """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from a steady state `ss`.""" return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) From ef4ae21f18338d9e260551a4752887af75761a7c Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 19 Aug 2021 12:23:08 -0500 Subject: [PATCH 221/288] Improve compare_steady_states function and move it to support module --- src/sequence_jacobian/devtools/__init__.py | 2 +- src/sequence_jacobian/devtools/upgrade.py | 39 -------------- src/sequence_jacobian/steady_state/support.py | 51 +++++++++++++++++++ 3 files changed, 52 insertions(+), 40 deletions(-) delete mode 100644 src/sequence_jacobian/devtools/upgrade.py diff --git a/src/sequence_jacobian/devtools/__init__.py b/src/sequence_jacobian/devtools/__init__.py index 9795de6..3f21969 100644 --- a/src/sequence_jacobian/devtools/__init__.py +++ b/src/sequence_jacobian/devtools/__init__.py @@ -1,3 +1,3 @@ """Tools for debugging, developing, and deprecating code""" -from . import analysis, debug, deprecate, upgrade +from . import analysis, debug, deprecate diff --git a/src/sequence_jacobian/devtools/upgrade.py b/src/sequence_jacobian/devtools/upgrade.py deleted file mode 100644 index e2994d8..0000000 --- a/src/sequence_jacobian/devtools/upgrade.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Tools for upgrading from older SSJ code conventions""" - -# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, and who -# may want additional support/tools for ensuring that their attempts to upgrade to use newer versions of -# sequence-jacobian has been successfully. - -import numpy as np - - -def compare_steady_states(ss_ref, ss_comp, name_map=None, verbose=True): - """ - This code is meant to provide a quick comparison of `ss_ref` the reference steady state dict from old code, and - `ss_comp` the steady state computed from the newer code. - """ - if name_map is None: - name_map = {} - - # Compare the steady state values present in both ss_ref and ss_comp - for key_ref in ss_ref.keys(): - if key_ref in ss_comp.keys(): - key_comp = key_ref - elif key_ref in name_map: - key_comp = name_map[key_ref] - else: - continue - if verbose: - if np.isscalar(ss_ref[key_ref]): - print(f"{key_ref} resid: {abs(ss_ref[key_ref] - ss_comp[key_comp])}") - else: - print(f"{key_ref} resid: {np.linalg.norm(ss_ref[key_ref].flatten() - ss_comp[key_comp].flatten(), np.inf)}") - else: - assert np.isclose(ss_ref[key_ref], ss_comp[key_comp]) - - # Show the steady state values present in only one of ss_ref or ss_comp - ss_ref_incl_mapped = set(ss_ref.keys()) - set(name_map.keys()) - ss_comp_incl_mapped = set(ss_comp.keys()) - set(name_map.values()) - diff_keys = ss_ref_incl_mapped.symmetric_difference(ss_comp_incl_mapped) - if diff_keys: - print(f"The keys present in only one of the two steady state dicts are {diff_keys}") diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py index 88374b6..7bcc3cb 100644 --- a/src/sequence_jacobian/steady_state/support.py +++ b/src/sequence_jacobian/steady_state/support.py @@ -95,6 +95,57 @@ def compute_target_values(targets, potential_args): return target_values +def compare_steady_states(ss_ref, ss_comp, tol=1e-8, name_map=None, internal=True, check_same_keys=True, verbose=False): + """Check if two steady state dicts (can be flat dicts or SteadyStateDict objects) are the same up to a tolerance""" + if name_map is None: + name_map = {} + + valid = True + + # Compare the steady state values present in both ss_ref and ss_comp + if internal: + if not hasattr(ss_ref, "internal") or not hasattr(ss_comp, "internal"): + warnings.warn("The provided steady state dicts do not both have .internal attrs. Will only compare" + " top-level values") + ds_to_check = [(ss_ref, ss_comp, "toplevel")] + else: + ds_to_check = [(ss_ref, ss_comp, "toplevel")] + [(ss_ref.internal[i], ss_comp.internal[i], i + "_internal") for i in ss_ref.internal] + else: + ds_to_check = [(ss_ref, ss_comp, "toplevel")] + + for ds in ds_to_check: + d_ref, d_comp, level = ds + for key_ref in d_ref.keys(): + if key_ref in d_comp.keys(): + key_comp = key_ref + elif key_ref in name_map: + key_comp = name_map[key_ref] + else: + continue + + if np.isscalar(d_ref[key_ref]): + resid = abs(d_ref[key_ref] - d_comp[key_comp]) + else: + resid = np.linalg.norm(d_ref[key_ref].ravel() - d_comp[key_comp].ravel(), np.inf) + if verbose: + print(f"{key_ref} resid: {resid}") + else: + if not np.all(np.isclose(resid, 0., atol=tol)): + valid = False + + # Show the steady state values present in only one of d_ref or d_comp, i.e. if there are missing keys + if check_same_keys: + d_ref_incl_mapped = set(d_ref.keys()) - set(name_map.keys()) + d_comp_incl_mapped = set(d_comp.keys()) - set(name_map.values()) + diff_keys = d_ref_incl_mapped.symmetric_difference(d_comp_incl_mapped) + if diff_keys: + if verbose: + print(f"At level '{level}', the keys present only one of the two steady state dicts are {diff_keys}") + valid = False + + return valid + + def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): """Find the set of unknowns that the `helper_blocks` solve for""" unknowns_handled_by_helpers = {} From d0ffa9df24f8495974340462ebbb0f97752fe5cd Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 19 Aug 2021 17:50:59 -0400 Subject: [PATCH 222/288] Refactoring of .impulse_linear is almost done. Status: hetoutput caused some trouble, should be easy to fix analogously to .jacobian. --- nesting_test.py | 40 +++++++----- .../blocks/combined_block.py | 21 ++----- src/sequence_jacobian/blocks/het_block.py | 10 +-- src/sequence_jacobian/blocks/simple_block.py | 13 ++-- src/sequence_jacobian/blocks/solved_block.py | 19 +++--- .../blocks/support/bijection.py | 5 +- .../blocks/support/impulse.py | 35 +++++++---- src/sequence_jacobian/jacobian/classes.py | 49 ++++++++++++--- src/sequence_jacobian/primitives.py | 62 ++++++++++--------- 9 files changed, 148 insertions(+), 106 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index f7b3845..6d90039 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -2,6 +2,7 @@ import sequence_jacobian as sj from sequence_jacobian import het, simple, hetoutput, combine, solved, create_model, get_H_U from sequence_jacobian.jacobian.classes import JacobianDict, ZeroMatrix +from sequence_jacobian.blocks.support.impulse import ImpulseDict '''Part 1: Household block''' @@ -142,24 +143,24 @@ def mkt_clearing(A, B, C, Y, G): 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143} calibration['rho_B'] = 0.8 -ss0 = dag.solve_steady_state(calibration, solver='hybr', - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +# ss0 = dag.solve_steady_state(calibration, solver='hybr', +# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, +# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) #Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) -# @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -# def fiscal_solved(B, G, rb, Y, transfer, rho_B): -# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B -# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised -# tau = rev / Y -# return B_rule, rev, tau +@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +def fiscal_solved(B, G, rb, Y, transfer, rho_B): + B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau -# dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -# ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', -# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, -# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) # assert all(np.allclose(ss0[k], ss[k]) for k in ss0) @@ -198,10 +199,17 @@ def mkt_clearing(A, B, C, Y, G): # J_all2 = J_int.compose(J_hh) -G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) +# G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) + +G = dag.solve_jacobian(ss, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300) + +shock = ImpulseDict({'r': 0.001*0.9**np.arange(300)}) + +td1 = G @ shock + -# td_lin = dag.solve_impulse_linear(ss, {'r': 0.001*0.9**np.arange(300)}, -# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) +td_lin = dag.solve_impulse_linear(ss, unknowns=['Y'], targets=['asset_mkt'], + inputs=shock, outputs=['A', 'Y', 'asset_mkt', 'goods_mkt']) # td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, # unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index c447b4b..b2566bf 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -62,7 +62,6 @@ def __repr__(self): return f"" def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): - """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" if helper_blocks is None: helper_blocks = [] @@ -79,8 +78,6 @@ def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): return ss def _impulse_nonlinear(self, ss, exogenous, **kwargs): - """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks from - a steady state, `ss`""" irf_nonlin_partial_eq = deepcopy(exogenous) for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} @@ -90,20 +87,17 @@ def _impulse_nonlinear(self, ss, exogenous, **kwargs): return ImpulseDict(irf_nonlin_partial_eq) - def _impulse_linear(self, ss, exogenous, T=None, Js=None): - """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks from - a steady_state, `ss`""" - irf_lin_partial_eq = deepcopy(exogenous) + def _impulse_linear(self, ss, inputs, outputs, Js): + irf_lin_partial_eq = deepcopy(inputs) for block in self.blocks: - input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} + input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update({k: v for k, v in block.impulse_linear(ss, input_args, T=T, Js=Js)}) + irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, block.outputs, Js)) return ImpulseDict(irf_lin_partial_eq) def _partial_jacobians(self, ss, inputs, outputs, T, Js): - """Calculate partial Jacobians (i.e. without forward accumulation) wrt `inputs` and outputs of other blocks.""" # Add intermediate inputs; remove vector-valued inputs vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued @@ -121,13 +115,11 @@ def _partial_jacobians(self, ss, inputs, outputs, T, Js): return curlyJs def _jacobian(self, ss, inputs, outputs, T, Js={}): - """Calculate a partial equilibrium Jacobian with respect to a set of `exogenous` shocks at - a steady state, `ss`""" # _partial_jacobians should calculate partial jacobians with exactly the inputs and outputs we want Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) original_outputs = outputs - total_Js = JacobianDict.identity(inputs, T=T) + total_Js = JacobianDict.identity(inputs) # horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! vector_valued = ss._vector_valued() @@ -146,9 +138,6 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, sort_blocks=False, **kwargs): - """Evaluate a general equilibrium steady state of the CombinedBlock given a `calibration` - and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and - the target conditions that must hold in general equilibrium""" if solver is None: solver = provide_solver_default(unknowns) if helper_blocks and sort_blocks is False: diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 6d82c2b..1abf725 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -347,14 +347,10 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - def _impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) + def _impulse_linear(self, ss, inputs, outputs, Js): + return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) + def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 32846d1..2d51f73 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -6,10 +6,9 @@ from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .support.impulse import ImpulseDict -from .support.bijection import Bijection from ..primitives import Block from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix, verify_saved_jacobian +from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix from ..utilities import misc from ..utilities.function import ExtendedFunction @@ -39,9 +38,6 @@ def __init__(self, f): super().__init__() self.f = ExtendedFunction(f) self.name = self.f.name - # TODO: if we do OrderedSet here instead of set, things break! - #self.inputs = set(self.f.inputs) - #self.outputs = set(self.f.outputs) self.inputs = self.f.inputs self.outputs = self.f.outputs @@ -65,8 +61,8 @@ def _impulse_nonlinear(self, ss, exogenous): return ImpulseDict(make_impulse_uniform_length(self.f(input_args))) - ss - def _impulse_linear(self, ss, exogenous, T=None, Js=None): - return ImpulseDict(self.jacobian(ss, exogenous=list(exogenous.keys()), T=T, Js=Js).apply(exogenous)) + def _impulse_linear(self, ss, inputs, outputs, Js): + return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) def _jacobian(self, ss, inputs, outputs, T): invertedJ = {i: {} for i in inputs} @@ -87,8 +83,7 @@ def _jacobian(self, ss, inputs, outputs, T): else: J[o][i] = invertedJ[i][o] - to_return = JacobianDict(J, name=self.name)[outputs, :] - return JacobianDict(J, name=self.name)[outputs, :] + return JacobianDict(J, name=self.name, T=T)[outputs, :] def compute_single_shock_J(self, ss, i): input_args = {i: ignore(ss[i]) for i in self.inputs} diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 19d5edd..4dbef14 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -1,5 +1,7 @@ import warnings +from sequence_jacobian.utilities.ordered_set import OrderedSet + from ..primitives import Block from ..blocks.simple_block import simple from ..blocks.parent import Parent @@ -77,21 +79,22 @@ def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, retur targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) - def _impulse_linear(self, ss, exogenous, T=None, Js=None): - return self.block.solve_impulse_linear(ss, exogenous=exogenous, unknowns=list(self.unknowns.keys()), - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - T=T, Js=Js) + def _impulse_linear(self, ss, inputs, outputs, Js): + return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), inputs, outputs, Js) def _jacobian(self, ss, inputs, outputs, T, Js): - return self.block.solve_jacobian(ss, set(self.unknowns), set(self.targets), inputs, outputs, T, Js)[outputs] + return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), + inputs, outputs, T, Js)[outputs] def _partial_jacobians(self, ss, inputs, outputs, T, Js={}): # call it on the child first - inner_Js = self.block.partial_jacobians(ss, inputs=(set(self.unknowns) | inputs), - outputs=(set(self.targets) | outputs), T=T, Js=Js) + inner_Js = self.block.partial_jacobians(ss, + inputs=(OrderedSet(self.unknowns) | inputs), + outputs=(OrderedSet(self.targets) | outputs - self.unknowns.keys()), + T=T, Js=Js) # with these inner Js, also compute H_U and factorize - H_U = self.block.jacobian(ss, inputs=self.unknowns.keys(), outputs=self.targets.keys(), T=T, Js=inner_Js) + H_U = self.block.jacobian(ss, inputs=OrderedSet(self.unknowns), outputs=OrderedSet(self.targets), T=T, Js=inner_Js) H_U_factored = FactoredJacobianDict(H_U, T) return {**inner_Js, self.name: H_U_factored} diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/blocks/support/bijection.py index 67bee0b..60d8803 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/blocks/support/bijection.py @@ -59,4 +59,7 @@ def __rmatmul__(self, x): elif isinstance(x, tuple): return tuple(self[k] for k in x) else: - return NotImplemented \ No newline at end of file + return NotImplemented + + def __bool__(self): + return bool(self.map) \ No newline at end of file diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index ecea31f..e4daf33 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -20,22 +20,18 @@ def __init__(self, impulse, T=None): T = self.infer_length() self.T = T - def keys(self): - return self.impulse.keys() - - def pack(self): - T = self.T - bigv = np.empty(T*len(self.impulse)) - for i, v in enumerate(self.impulse.values()): - bigv[i*T:(i+1)*T] = v - return bigv - def __repr__(self): return f'' def __iter__(self): return iter(self.impulse) + def items(self): + return self.impulse.items() + + def update(self, other): + return self.impulse.update(other.impulse) + def __or__(self, other): if not isinstance(other, ImpulseDict): raise ValueError('Trying to merge an ImpulseDict with something else.') @@ -108,9 +104,26 @@ def __matmul__(self, x): def __rmatmul__(self, x): return self.__matmul__(x) + def keys(self): + return self.impulse.keys() + + def pack(self): + T = self.T + bigv = np.empty(T*len(self.impulse)) + for i, v in enumerate(self.impulse.values()): + bigv[i*T:(i+1)*T] = v + return bigv + + @staticmethod + def unpack(bigv, outputs, T): + impulse = {} + for i, o in enumerate(outputs): + impulse[o] = bigv[i*T:(i+1)*T] + return ImpulseDict(impulse) + def infer_length(self): lengths = [len(v) for v in self.impulse.values()] length = max(lengths) - if lengths != min(lengths): + if length != min(lengths): raise ValueError(f'Building ImpulseDict with inconsistent lengths {max(lengths)} and {min(lengths)}') return length diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index bda9153..03f6384 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -5,14 +5,15 @@ import warnings import numpy as np -from sequence_jacobian.primitives import Array - from . import support from ..utilities.misc import factor, factored_solve from ..utilities.ordered_set import OrderedSet from ..blocks.support.bijection import Bijection from ..blocks.support.impulse import ImpulseDict -from typing import Dict, Union +from typing import Any, Dict, Union + +# Basic types +Array = Any class Jacobian(metaclass=ABCMeta): @@ -401,16 +402,21 @@ def __matmul__(self, x): if isinstance(x, JacobianDict): return self.compose(x) elif isinstance(x, Bijection): - nesteddict = x @ self.nesteddict - for o in nesteddict.keys(): - nesteddict[o] = x @ nesteddict[o] - return JacobianDict(nesteddict, inputs=x @ self.inputs, outputs=x @ self.outputs) + return self.remap(x) else: return self.apply(x) def __rmatmul__(self, x): if isinstance(x, Bijection): - return JacobianDict(x @ self.nesteddict, inputs=x @ self.inputs, outputs=x @ self.outputs) + return self.remap(x) + + def remap(self, x: Bijection): + if not x: + return self + nesteddict = x @ self.nesteddict + for o in nesteddict.keys(): + nesteddict[o] = x @ nesteddict[o] + return JacobianDict(nesteddict, inputs=x @ self.inputs, outputs=x @ self.outputs) def __bool__(self): return bool(self.outputs) and bool(self.inputs) @@ -479,7 +485,7 @@ def unpack(bigjac, outputs, inputs, T): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] return JacobianDict(jacdict, outputs, inputs, T=T) -class FactoredJacobianDict(JacobianDict): +class FactoredJacobianDict: def __init__(self, jacobian_dict: JacobianDict, T=None): if jacobian_dict.T is None: if T is None: @@ -499,6 +505,30 @@ def __init__(self, jacobian_dict: JacobianDict, T=None): def __repr__(self): return f'<{type(self).__name__} unknowns={self.unknowns}, targets={self.targets}>' + # TODO: test this + def to_jacobian_dict(self): + return JacobianDict.unpack(-factored_solve(self.H_U_factored, np.eye(self.T*len(self.unknowns))), self.unknowns, self.targets, self.T) + + def __matmul__(self, x): + if isinstance(x, JacobianDict): + return self.compose(x) + elif isinstance(x, Bijection): + return self.remap(x) + else: + return self.apply(x) + + def __rmatmul__(self, x): + if isinstance(x, Bijection): + return self.remap(x) + + def remap(self, x: Bijection): + if not x: + return self + newself = copy.copy(self) + newself.unknowns = x @ self.unknowns + newself.targets = x @ self.targets + return newself + def compose(self, J: JacobianDict): # take intersection of J outputs with self.targets # then pack that reduced J into a matrix, then apply lu_solve, then unpack @@ -580,4 +610,3 @@ def verify_saved_jacobian(block_name, Js, outputs, inputs, T): return False return True - diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 0cb4168..9b350c7 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -11,8 +11,8 @@ from .steady_state.drivers import steady_state as ss from .steady_state.support import provide_solver_default -from .nonlinear import td_solve -from .jacobian.drivers import get_impulse, get_G +# from .nonlinear import td_solve +# from .jacobian.drivers import get_impulse, get_G from .steady_state.classes import SteadyStateDict, UserProvidedSS from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict @@ -96,10 +96,13 @@ def impulse_nonlinear(self, ss: SteadyStateDict, from a steady state `ss`.""" return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) - def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js={}) -> ImpulseDict: - """Calculate a partial equilibrium, linear impulse response to a set of `exogenous` shocks - from a steady state `ss`.""" - return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) + def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], + outputs: Optional[List[str]] = None, Js={}) -> ImpulseDict: + """Calculate a partial equilibrium, linear impulse response of `outputs` to a set of shocks in `inputs` + around a steady state `ss`.""" + inputs = ImpulseDict(inputs) + _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js) def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): # TODO: annotate signature @@ -107,11 +110,11 @@ def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): inputs = self.inputs if outputs is None: outputs = self.outputs - inputs, outputs = set(inputs), set(outputs) + inputs, outputs = inputs, outputs # if you have a J for this block that already has everything you need, use it # TODO: add check for T, maybe look at verify_saved_jacobian for ideas? - if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): + if (self.name in Js) and isinstance(Js[self.name], JacobianDict) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): return {self.name: Js[self.name][outputs, inputs]} # if it's a leaf, just call Jacobian method, include if nonzero @@ -129,10 +132,9 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis T: Optional[int] = None, Js={}) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) - inputs, outputs = OrderedSet(inputs), OrderedSet(outputs) # if you have a J for this block that has everything you need, use it - if (self.name in Js) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): + if (self.name in Js) and isinstance(Js[self.name], JacobianDict) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): return Js[self.name][outputs, inputs] # if it's a leaf, call Jacobian method, don't supply Js @@ -171,23 +173,27 @@ def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], unknowns=unknowns, targets=targets, Js=Js, **kwargs) return ImpulseDict(irf_nonlin_gen_eq) - def solve_impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], - unknowns: List[str], targets: List[str], - T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = {}, - **kwargs) -> ImpulseDict: - """Calculate a general equilibrium, linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], + inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]], + Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: + """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - irf_lin_gen_eq = get_impulse(blocks, exogenous, unknowns, targets, T=T, ss=ss, Js=Js, **kwargs) - return ImpulseDict(irf_lin_gen_eq) + inputs = ImpulseDict(inputs) + input_names, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + + Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, inputs.T, Js) + + H_U = self.jacobian(ss, unknowns, targets, inputs.T, Js).pack(inputs.T) + dH = self.impulse_linear(ss, inputs, targets, Js).pack() + dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, inputs.T) + + return self.impulse_linear(ss, dU | inputs, outputs - unknowns, Js) + - def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], targets: List[str], + def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = {}, - **kwargs) -> JacobianDict: + Js: Optional[Dict[str, JacobianDict]] = {}) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -196,9 +202,9 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], T = 300 inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) - inputs, unknowns, targets = list(inputs), list(unknowns), list(targets) + unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) - Js = self.partial_jacobians(ss, set(inputs) | set(unknowns), (set(outputs) | set(targets)) - set(unknowns), T, Js) + Js = self.partial_jacobians(ss, inputs | unknowns, (outputs | targets) - unknowns, T, Js) H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) @@ -206,7 +212,7 @@ def solve_jacobian(self, ss: Dict[str, Union[Real, Array]], unknowns: List[str], from . import combine self_with_unknowns = combine([U_Z, self]) - return self_with_unknowns.jacobian(ss, inputs, set(unknowns) | set(outputs), T, Js) + return self_with_unknowns.jacobian(ss, inputs, unknowns | outputs, T, Js) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: @@ -240,4 +246,4 @@ def default_inputs_outputs(self, ss: SteadyStateDict, inputs, outputs): inputs = self.inputs if outputs is None: outputs = self.outputs - ss._vector_valued() - return inputs, outputs + return OrderedSet(inputs), OrderedSet(outputs) From d4edf80616b52b97c9f99cdb21fda4873b67e3ae Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 20 Aug 2021 12:31:28 -0500 Subject: [PATCH 223/288] WIP initial implementation of RedirectedBlock (replacing helper block functionality) and simplification of steady state code --- .../auxiliary_blocks/redirected_block.py | 90 ++++++++++ .../blocks/combined_block.py | 13 +- src/sequence_jacobian/primitives.py | 18 +- src/sequence_jacobian/steady_state/drivers.py | 158 +++++++++--------- 4 files changed, 188 insertions(+), 91 deletions(-) create mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py new file mode 100644 index 0000000..1e39200 --- /dev/null +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py @@ -0,0 +1,90 @@ +"""An auxiliary Block class for altering the direction of some of the equations in a given Block object""" + +import numpy as np + +from ..combined_block import CombinedBlock +from ..parent import Parent +from ...primitives import Block +from ...utilities.ordered_set import OrderedSet + + +class RedirectedBlock(CombinedBlock): + """A RedirectedBlock is a Block where a subset of the input-output mappings are altered, or 'redirected'. + This is useful when computing steady states in particular, where often we will want to set target values + explicitly and back out what are the implied values of the unknowns that justify those targets.""" + def __init__(self, block, redirect_block): + Block.__init__(self) + + # TODO: Figure out what are the criteria we want to require of the helper block + # if not redirect_block.inputs & block.outputs: + # raise ValueError("User-provided redirect_block must ") + # assert redirect_block.outputs <= (block.inputs | block.outputs) + self.directed = block + # TODO: Implement case with multiple redirecting (helper) blocks later. + # If `block` is a non-nested Block then multiple helper blocks may seem a bit redundant, since + # helper blocks should typically be sorted before the `block` they are associated to (CHECK THIS), + # but multiple helper blocks may be necessary when `block` is a nested Block, i.e. if helpers need to + # be inserted at different stages of the DAG. Then we also need to do some non-trivial sorting when + # filling out the self.blocks attribute + self.redirected = redirect_block + self.blocks = [redirect_block, block] + + self.name = block.name + "_redirect" + + # now that it has a name, do Parent initialization + Parent.__init__(self, [block, redirect_block]) + + self.inputs = (redirect_block.inputs | block.inputs) - redirect_block.outputs + self.outputs = (redirect_block.outputs | block.outputs) - redirect_block.inputs + + # Calculate what are the inputs and outputs of the Block objects underlying `self`, without + # any of the redirecting blocks. + if not isinstance(self.directed, Parent): + self.inputs_directed = self.directed.inputs + self.outputs_directed = self.directed.outputs + else: + inputs_directed, outputs_directed = OrderedSet({}), OrderedSet({}) + ps_checked = set({}) + for d in self.directed.descendants: + # The descendant's parent's name (if it has one, o/w the descendant's name) + p = self.directed.descendants[d] + if p is None or p in ps_checked: + continue + else: + ps_checked |= set(p) + if hasattr(self.directed[p], "directed"): + inputs_directed |= self.directed[p].directed.inputs + outputs_directed |= self.directed[p].directed.outputs + else: + inputs_directed |= self[d].inputs + outputs_directed |= self[d].outputs + self.inputs_directed = inputs_directed - outputs_directed + self.outputs_directed = outputs_directed + + def __repr__(self): + return f"" + + def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): + """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" + ss = calibration.copy() + + if not bypass_redirection: + for block in self.blocks: + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) + ss.update(outputs) + else: + inner_dissolve = [k for k in dissolve if self.descendants[k] == self.directed.name] + outputs = self.directed.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) + ss.update(outputs) + + return ss + + # TODO: May not even need this! Just pass in bypass_redirection at the top-level block.steady_state call + # and check it against targets. + def validate_steady_state(self, calibration, targets, ctol=1e-8, **kwargs): + targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + ss_val = self.directed.steady_state(self.steady_state(calibration, **kwargs), bypass_redirection=True, **kwargs) + if not np.all([np.abs(ss_val[k] - targets[k]) < ctol for k in targets]): + raise RuntimeError(f"{self.directed.name}'s steady state does not hit the targets with the provided calibration") diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 54a16b9..238caad 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -61,19 +61,14 @@ def __repr__(self): else: return f"" - def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): + def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" - if helper_blocks is None: - helper_blocks = [] - - topsorted = utils.graph.block_sort(self.blocks, calibration=calibration, helper_blocks=helper_blocks) - blocks_all = self.blocks + helper_blocks ss = deepcopy(calibration) - for i in topsorted: + for block in self.blocks: # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == blocks_all[i].name] - outputs = blocks_all[i].steady_state(ss, dissolve=inner_dissolve, **kwargs) + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) ss.update(outputs) return ss diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index df46a90..e95b3b8 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -77,16 +77,26 @@ def outputs(self): pass def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], - dissolve: Optional[List[str]] = [], **kwargs) -> SteadyStateDict: + dissolve: Optional[List[str]] = [], bypass_redirection: bool = False, + **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" - # special handling: add all unknowns of dissolved blocks to inputs - inputs = self.inputs.copy() + # Special handling: 1) Find inputs/outputs of the Block w/o redirection + # 2) Add all unknowns of dissolved blocks to inputs + if bypass_redirection and isinstance(self, Parent): + if hasattr(self, "inputs_directed"): + inputs = self.directed.inputs.copy() + else: + inputs = OrderedSet({}) + for d in self.descendants: + inputs |= self[d].inputs_directed.copy() if hasattr(self, "inputs_directed") else self[d].inputs.copy() + else: + inputs = self.inputs.copy() if isinstance(self, Parent): for k in dissolve: inputs |= self.get_attribute(k, 'unknowns').keys() calibration = SteadyStateDict(calibration)[inputs] - kwargs['dissolve'] = dissolve + kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index 549937b..7f90295 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -14,9 +14,9 @@ # Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, dissolve=[], +def steady_state(blocks, calibration, unknowns, targets, dissolve=None, sort_blocks=True, helper_blocks=None, helper_targets=None, - consistency_check=True, ttol=2e-12, ctol=1e-9, fragile=False, + consistency_check=True, ttol=2e-12, ctol=1e-9, block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None): """ @@ -48,8 +48,6 @@ def steady_state(blocks, calibration, unknowns, targets, dissolve=[], ctol: `float` The tolerance for the consistency check---how close the user wants the computed target values, without the use of helper blocks, to equal the desired values - fragile: `bool` - Throw errors instead of warnings when certain criteria are not met, i.e if the consistency_check fails block_kwargs: `dict` A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that @@ -77,67 +75,71 @@ def steady_state(blocks, calibration, unknowns, targets, dissolve=[], block_kwargs, solver_kwargs, constrained_kwargs) # Initial setup of blocks, targets, and dictionary of steady state values to be returned - blocks_all = blocks + helper_blocks + # blocks_all = blocks + helper_blocks + blocks_all = blocks targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - helper_targets = {t: targets[t] for t in targets if t in helper_targets} - helper_outputs = {} + # helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) + # helper_targets = {t: targets[t] for t in targets if t in helper_targets} + # helper_outputs = {} ss_values = SteadyStateDict(calibration) - ss_values.update(helper_targets) + # ss_values.update(helper_targets) - if sort_blocks: - topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) - else: - topsorted = range(len(blocks + helper_blocks)) + # if sort_blocks: + # topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) + # else: + # topsorted = range(len(blocks + helper_blocks)) - def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, consistency_check=False): + def residual(targets_dict, unknown_keys, unknown_values, bypass_redirection=False): ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper # block subsetting # Progress through the DAG computing the resulting steady state values based on the unknown_values # provided to the residual function - for i in topsorted: - if not include_helpers and blocks_all[i] in helper_blocks: - continue + for block in blocks_all: + # if not include_helpers and blocks_all[i] in helper_blocks: + # continue # TODO: this is duplicate of CombinedBlock inner_dissolve, should offload to that - inner_dissolve = [k for k in dissolve if isinstance(blocks_all[i], Parent) and k in blocks_all[i].descendants] - outputs = blocks_all[i].steady_state(ss_values, hetoutput=True, - dissolve=inner_dissolve, verbose=verbose, **block_kwargs) - if include_helpers and blocks_all[i] in helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) - ss_values.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) + inner_dissolve = [k for k in dissolve if isinstance(block, Parent) and k in block.descendants] + outputs = block.steady_state(ss_values, hetoutput=True, dissolve=inner_dissolve, + bypass_redirection=bypass_redirection, verbose=verbose, **block_kwargs) + # if include_helpers and blocks_all[i] in helper_blocks: + # helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) + # ss_values.update(outputs) + # else: + # # Don't overwrite entries in ss_values corresponding to what has already + # # been solved for in helper_blocks so we can check for consistency after-the-fact + # ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) + ss_values.update(outputs) # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the # dict of ss_values to compute the "unknown_solutions" return compute_target_values(targets_dict, ss_values) - if helper_blocks: - unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, - helper_targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=ttol, verbose=verbose, fragile=fragile) - else: - unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=ttol, verbose=verbose, fragile=fragile) + # if helper_blocks: + # unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, + # helper_targets, solver, solver_kwargs, + # constrained_method=constrained_method, + # constrained_kwargs=constrained_kwargs, + # tol=ttol, verbose=verbose, fragile=fragile) + # else: + unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + constrained_method=constrained_method, + constrained_kwargs=constrained_kwargs, + tol=ttol, verbose=verbose) # Check that the solution is consistent with what would come out of the DAG without the helper blocks - if consistency_check and helper_blocks: + if consistency_check: # Add the unknowns not handled by helpers into the DAG to be checked. unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) - cresid = abs(np.max(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), - include_helpers=False, consistency_check=True))) - run_consistency_check(cresid, ctol=ctol, fragile=fragile) + cresid = np.max(abs(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), + bypass_redirection=True))) + if cresid > ctol: + raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" + f" the consistency of the DAG without redirection.") # Update to set the solutions for the steady state values of the unknowns ss_values.update(unknowns_solved) @@ -178,7 +180,7 @@ def residual(targets_dict, unknown_keys, unknown_values, include_helpers=True, c def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None, - tol=2e-12, verbose=False, fragile=False): + tol=2e-12, verbose=False): """ Given a residual function (constructed within steady_state) and a set of bounds or initial values for the set of unknowns, solve for the root. @@ -224,7 +226,7 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi raise ValueError(f"Steady-state solver, {solver}, did not converge.") unknown_solutions = result.root elif solver in scipy_optimize_multi_solvers: - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns, fragile=fragile) + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) # If no bounds were provided if not bounds: result = opt.root(residual_f, initial_values, @@ -278,37 +280,37 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) -def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, - solver, solver_kwargs, constrained_method="linear_continuation", - constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): - """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets - with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" - # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, - # to populate the `ss_values` dict with the unknown values that: - # a) are handled by helper blocks and b) are excludable from the main DAG - # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed - unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] - targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) - - # Subset out the unknowns and targets that are not excludable from the main DAG loop - unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) - targets_non_excl = misc.dict_diff(targets, helper_targets) - - # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of - # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and - # we can omit them and their corresponding `unknowns` in the main DAG. - if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): - unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, - solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=tol, verbose=verbose, fragile=fragile) - # If targets handled by helpers and excludable from the main DAG are not satisfied then - # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, - # and they remain a part of the main DAG when solving. - else: - unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=tol, verbose=verbose, fragile=fragile) - return unknown_solutions +# def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, +# solver, solver_kwargs, constrained_method="linear_continuation", +# constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): +# """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets +# with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" +# # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, +# # to populate the `ss_values` dict with the unknown values that: +# # a) are handled by helper blocks and b) are excludable from the main DAG +# # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed +# unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] +# targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) +# +# # Subset out the unknowns and targets that are not excludable from the main DAG loop +# unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) +# targets_non_excl = misc.dict_diff(targets, helper_targets) +# +# # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of +# # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and +# # we can omit them and their corresponding `unknowns` in the main DAG. +# if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): +# unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, +# solver, solver_kwargs, +# constrained_method=constrained_method, +# constrained_kwargs=constrained_kwargs, +# tol=tol, verbose=verbose, fragile=fragile) +# # If targets handled by helpers and excludable from the main DAG are not satisfied then +# # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, +# # and they remain a part of the main DAG when solving. +# else: +# unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, +# constrained_method=constrained_method, +# constrained_kwargs=constrained_kwargs, +# tol=tol, verbose=verbose, fragile=fragile) +# return unknown_solutions From 71af74e07231b8a943d150d43733dff793e9f4e3 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 20 Aug 2021 12:59:35 -0500 Subject: [PATCH 224/288] Simplify _steady_state method of RedirectedBlock --- .../auxiliary_blocks/redirected_block.py | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py index 1e39200..d21d932 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py @@ -66,25 +66,8 @@ def __repr__(self): def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" - ss = calibration.copy() - - if not bypass_redirection: - for block in self.blocks: - # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) - ss.update(outputs) + if bypass_redirection: + kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection + return self.directed._steady_state(calibration, **{k: v for k, v in kwargs.items() if k in self.directed.ss_valid_input_kwargs}) else: - inner_dissolve = [k for k in dissolve if self.descendants[k] == self.directed.name] - outputs = self.directed.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) - ss.update(outputs) - - return ss - - # TODO: May not even need this! Just pass in bypass_redirection at the top-level block.steady_state call - # and check it against targets. - def validate_steady_state(self, calibration, targets, ctol=1e-8, **kwargs): - targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - ss_val = self.directed.steady_state(self.steady_state(calibration, **kwargs), bypass_redirection=True, **kwargs) - if not np.all([np.abs(ss_val[k] - targets[k]) < ctol for k in targets]): - raise RuntimeError(f"{self.directed.name}'s steady state does not hit the targets with the provided calibration") + return super()._steady_state(calibration, dissolve=dissolve, bypass_redirection=bypass_redirection, **kwargs) From 87386e6ee16307d01a8e7b05da6656c7ecda8012 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Fri, 20 Aug 2021 13:51:00 -0500 Subject: [PATCH 225/288] Simplify residual function in steady_state driver to use context info from its enclosing namespace as opposed to as direct args --- src/sequence_jacobian/steady_state/drivers.py | 54 ++++--------------- src/sequence_jacobian/steady_state/support.py | 17 +----- 2 files changed, 12 insertions(+), 59 deletions(-) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index 7f90295..bbee51c 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -70,62 +70,31 @@ def steady_state(blocks, calibration, unknowns, targets, dissolve=None, A dictionary containing all of the pre-specified values and computed values from the steady state computation """ - dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs =\ - instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, - block_kwargs, solver_kwargs, constrained_kwargs) + dissolve, block_kwargs, solver_kwargs, constrained_kwargs =\ + instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs) # Initial setup of blocks, targets, and dictionary of steady state values to be returned - # blocks_all = blocks + helper_blocks blocks_all = blocks targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - # helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - # helper_targets = {t: targets[t] for t in targets if t in helper_targets} - # helper_outputs = {} - ss_values = SteadyStateDict(calibration) - # ss_values.update(helper_targets) - - # if sort_blocks: - # topsorted = graph.block_sort(blocks, helper_blocks=helper_blocks, calibration=ss_values) - # else: - # topsorted = range(len(blocks + helper_blocks)) + unknown_keys = unknowns.keys() - def residual(targets_dict, unknown_keys, unknown_values, bypass_redirection=False): + def residual(unknown_values, bypass_redirection=False): ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper # block subsetting - # Progress through the DAG computing the resulting steady state values based on the unknown_values - # provided to the residual function for block in blocks_all: - # if not include_helpers and blocks_all[i] in helper_blocks: - # continue # TODO: this is duplicate of CombinedBlock inner_dissolve, should offload to that inner_dissolve = [k for k in dissolve if isinstance(block, Parent) and k in block.descendants] outputs = block.steady_state(ss_values, hetoutput=True, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, verbose=verbose, **block_kwargs) - # if include_helpers and blocks_all[i] in helper_blocks: - # helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in blocks_all[i].outputs | set(helper_targets.keys())}) - # ss_values.update(outputs) - # else: - # # Don't overwrite entries in ss_values corresponding to what has already - # # been solved for in helper_blocks so we can check for consistency after-the-fact - # ss_values.update(outputs) if consistency_check else ss_values.update(outputs.difference(helper_outputs)) ss_values.update(outputs) - # Because in solve_for_unknowns, models that are fully "solved" (i.e. RBC) require the - # dict of ss_values to compute the "unknown_solutions" - return compute_target_values(targets_dict, ss_values) + return compute_target_values(targets, ss_values) - # if helper_blocks: - # unknowns_solved = _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, - # helper_targets, solver, solver_kwargs, - # constrained_method=constrained_method, - # constrained_kwargs=constrained_kwargs, - # tol=ttol, verbose=verbose, fragile=fragile) - # else: - unknowns_solved = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, + unknowns_solved = _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs, tol=ttol, verbose=verbose) @@ -135,8 +104,7 @@ def residual(targets_dict, unknown_keys, unknown_values, bypass_redirection=Fals # Add the unknowns not handled by helpers into the DAG to be checked. unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) - cresid = np.max(abs(residual(targets, unknowns_solved.keys(), unknowns_solved.values(), - bypass_redirection=True))) + cresid = np.max(abs(residual(unknowns_solved.values(), bypass_redirection=True))) if cresid > ctol: raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" f" the consistency of the DAG without redirection.") @@ -178,7 +146,7 @@ def residual(targets_dict, unknown_keys, unknown_values, bypass_redirection=Fals # return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) -def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, residual_kwargs=None, +def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, residual_kwargs=None, constrained_method="linear_continuation", constrained_kwargs=None, tol=2e-12, verbose=False): """ @@ -211,10 +179,8 @@ def _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, resi scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", "excitingmixing", "krylov", "df-sane"] - # Construct a reduced residual function, which contains addl context of unknowns, targets, and keyword arguments. - # This is to bypass issues with passing a residual function that requires contextual, positional arguments - # separate from the unknown values that need to be solved for into the multivariate solvers - residual_f = partial(residual, targets, unknowns.keys(), **residual_kwargs) + # Wrap kwargs into the residual function + residual_f = partial(residual, **residual_kwargs) if solver is None: raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py index 7bcc3cb..f45471d 100644 --- a/src/sequence_jacobian/steady_state/support.py +++ b/src/sequence_jacobian/steady_state/support.py @@ -5,20 +5,10 @@ import numpy as np -def instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targets, - block_kwargs, solver_kwargs, constrained_kwargs): +def instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs): """Instantiate mutable types from `None` default values in the steady_state function""" if dissolve is None: dissolve = [] - if helper_blocks is None and helper_targets is None: - helper_blocks = [] - helper_targets = [] - elif helper_blocks is not None and helper_targets is None: - raise ValueError("If the user has provided `helper_blocks`, the kwarg `helper_targets` must be specified" - " indicating which target variables are handled by the `helper_blocks`.") - elif helper_blocks is None and helper_targets is not None: - raise ValueError("If the user has provided `helper_targets`, the kwarg `helper_blocks` must be specified" - " indicating which helper blocks handle the `helper_targets`") if block_kwargs is None: block_kwargs = {} if solver_kwargs is None: @@ -26,7 +16,7 @@ def instantiate_steady_state_mutable_kwargs(dissolve, helper_blocks, helper_targ if constrained_kwargs is None: constrained_kwargs = {} - return dissolve, helper_blocks, helper_targets, block_kwargs, solver_kwargs, constrained_kwargs + return dissolve, block_kwargs, solver_kwargs, constrained_kwargs def provide_solver_default(unknowns): @@ -84,9 +74,6 @@ def compute_target_values(targets, potential_args): target_values[i] = potential_args[t] - potential_args[v] else: target_values[i] = potential_args[t] - v - # TODO: Implement feature to allow for an arbitrary explicit function expression as a potential target value - # e.g. targets = {"goods_mkt": "Y - C - I"}, so long as the expression is only comprise of generic numerical - # operators and variables solved for along the DAG prior to reaching the target. # Univariate solvers require float return values (and not lists) if len(targets) == 1: From c83cf5700358b24fb3337e4d014133374d286cee Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Mon, 23 Aug 2021 13:58:21 -0500 Subject: [PATCH 226/288] Add note about being careful re: sort order for RedirectedBlock --- .../blocks/auxiliary_blocks/redirected_block.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py index d21d932..2d1de6a 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py @@ -39,6 +39,8 @@ def __init__(self, block, redirect_block): # Calculate what are the inputs and outputs of the Block objects underlying `self`, without # any of the redirecting blocks. + # TODO: Think more carefully about evaluating a DAG with bypass_redirection, if the redirecting + # blocks change the sort order of the DAG!! if not isinstance(self.directed, Parent): self.inputs_directed = self.directed.inputs self.outputs_directed = self.directed.outputs @@ -65,7 +67,7 @@ def __repr__(self): return f"" def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): - """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" + """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" if bypass_redirection: kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection return self.directed._steady_state(calibration, **{k: v for k, v in kwargs.items() if k in self.directed.ss_valid_input_kwargs}) From 39b8ed7590015a3d20396cdb24cdc01ec4d6d472 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 23 Aug 2021 18:13:22 -0400 Subject: [PATCH 227/288] .solve_impulse_linear works with solved_block, nested combined block --- nesting_test.py | 6 ++++-- src/sequence_jacobian/blocks/combined_block.py | 7 +++++-- src/sequence_jacobian/blocks/solved_block.py | 2 +- src/sequence_jacobian/blocks/support/impulse.py | 4 +++- src/sequence_jacobian/primitives.py | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 6d90039..6910dd4 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -201,7 +201,7 @@ def fiscal_solved(B, G, rb, Y, transfer, rho_B): # G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) -G = dag.solve_jacobian(ss, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300) +G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300) shock = ImpulseDict({'r': 0.001*0.9**np.arange(300)}) @@ -209,7 +209,9 @@ def fiscal_solved(B, G, rb, Y, transfer, rho_B): td_lin = dag.solve_impulse_linear(ss, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['A', 'Y', 'asset_mkt', 'goods_mkt']) + inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) + +assert all(np.allclose(td1[k], td_lin[k]) for k in td1) # td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, # unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index b2566bf..debbe3a 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -88,14 +88,17 @@ def _impulse_nonlinear(self, ss, exogenous, **kwargs): return ImpulseDict(irf_nonlin_partial_eq) def _impulse_linear(self, ss, inputs, outputs, Js): + original_outputs = outputs + outputs = (outputs | self._required) - ss._vector_valued() + irf_lin_partial_eq = deepcopy(inputs) for block in self.blocks: input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, block.outputs, Js)) + irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js)) - return ImpulseDict(irf_lin_partial_eq) + return ImpulseDict(irf_lin_partial_eq[original_outputs]) def _partial_jacobians(self, ss, inputs, outputs, T, Js): # Add intermediate inputs; remove vector-valued inputs diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 4dbef14..9eb81c8 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -80,7 +80,7 @@ def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, retur monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) def _impulse_linear(self, ss, inputs, outputs, Js): - return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), inputs, outputs, Js) + return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), inputs, outputs - self.unknowns.keys(), Js) def _jacobian(self, ss, inputs, outputs, T, Js): return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index e4daf33..ab93135 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -3,6 +3,8 @@ import numpy as np from copy import deepcopy +from sequence_jacobian.utilities.ordered_set import OrderedSet + from ...steady_state.classes import SteadyStateDict from .bijection import Bijection @@ -45,7 +47,7 @@ def __getitem__(self, item): if isinstance(item, str): # Case 1: ImpulseDict['C'] returns array return self.impulse[item] - elif isinstance(item, list): + elif isinstance(item, list) or isinstance(item, OrderedSet): # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts return type(self)({k: self.impulse[k] for k in item}) else: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 9b350c7..617e71b 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -188,7 +188,7 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets dH = self.impulse_linear(ss, inputs, targets, Js).pack() dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, inputs.T) - return self.impulse_linear(ss, dU | inputs, outputs - unknowns, Js) + return self.impulse_linear(ss, dU | inputs, outputs, Js) def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], From 82317fcd74b4f82b75991e61eb5bf0bcde09d35d Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 23 Aug 2021 18:19:19 -0400 Subject: [PATCH 228/288] fixed tests except test_transition_dynamics --- tests/base/test_public_classes.py | 2 +- tests/base/test_solved_block.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index f40aa1a..d1ea968 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -14,7 +14,7 @@ def test_impulsedict(krusell_smith_dag): T = 200 # Linearized impulse responses as deviations - ir_lin = ks_model.solve_impulse_linear(ss, {'Z': 0.01 * 0.5**np.arange(T)}, unknowns, targets) + ir_lin = ks_model.solve_impulse_linear(ss, unknowns, targets, inputs={'Z': 0.01 * 0.5**np.arange(T)}, outputs=['C', 'K', 'r']) # Get method assert isinstance(ir_lin, ImpulseDict) diff --git a/tests/base/test_solved_block.py b/tests/base/test_solved_block.py index 1c2eb7c..a10bd09 100644 --- a/tests/base/test_solved_block.py +++ b/tests/base/test_solved_block.py @@ -25,6 +25,6 @@ def myblock_solved(u, i): J_u = myblock.jacobian(ss, inputs=['u'], T=20) # square jac of underlying simple block J_factored = FactoredJacobianDict(J_u, T=20) J_i = myblock.jacobian(ss, inputs=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns -J2 = J_factored.compose(J_i, T=20) # obtain jac of unknown wrt to non-unknown inputs using factored jac +J2 = J_factored.compose(J_i) # obtain jac of unknown wrt to non-unknown inputs using factored jac assert np.allclose(J1['u']['i'], J2['u']['i']) \ No newline at end of file From cf0d4bc21a203622f37a986499223865ba1dcec8 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 24 Aug 2021 16:43:52 -0400 Subject: [PATCH 229/288] .impulse_nonlinear in progress; there's an issue with Displace.ss --- nesting_test.py | 113 ++++++------------ .../blocks/combined_block.py | 13 +- src/sequence_jacobian/blocks/het_block.py | 53 +++----- src/sequence_jacobian/blocks/simple_block.py | 7 +- src/sequence_jacobian/blocks/solved_block.py | 21 ++-- src/sequence_jacobian/jacobian/classes.py | 31 ++--- src/sequence_jacobian/primitives.py | 105 +++++++++++----- 7 files changed, 164 insertions(+), 179 deletions(-) diff --git a/nesting_test.py b/nesting_test.py index 6910dd4..08a428c 100644 --- a/nesting_test.py +++ b/nesting_test.py @@ -107,14 +107,22 @@ def interest_rates(r): return rpost, rb +@simple +def fiscal(B, G, rb, Y, transfer): + rev = rb * B + G + transfer # revenue to be raised + tau = rev / Y + return rev, tau + # @simple -# def fiscal(B, G, rb, Y, transfer): -# rev = rb * B + G + transfer # revenue to be raised +# def fiscal(B, G, rb, Y, transfer, rho_B): +# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B +# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised # tau = rev / Y -# return rev, tau +# return B_rule, rev, tau -@simple -def fiscal(B, G, rb, Y, transfer, rho_B): + +@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') +def fiscal_solved(B, G, rb, Y, transfer, rho_B): B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised tau = rev / Y @@ -128,90 +136,41 @@ def mkt_clearing(A, B, C, Y, G): return asset_mkt, goods_mkt -'''try this''' - -# flat dag -# dag = sj.create_model([household, income_state_vars, asset_state_vars, interest_rates, fiscal, mkt_clearing], -# name='HANK') +'''Try this''' - -# nested dag +# DAG with a nested CombinedBlock hh = combine([household, income_state_vars, asset_state_vars], name='HH') -dag = sj.create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') +dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') +# Calibrate steady state calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, - 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143} -calibration['rho_B'] = 0.8 - -# ss0 = dag.solve_steady_state(calibration, solver='hybr', -# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, -# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) - -#Js = dag.partial_jacobians(ss0, inputs=['Y', 'r'], T=10) - -@solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal_solved(B, G, rb, Y, transfer, rho_B): - B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau - -dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') + 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143, 'rho_B': 0.8} ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) +# ss0 = dag.solve_steady_state(calibration, solver='hybr', +# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, +# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) # assert all(np.allclose(ss0[k], ss[k]) for k in ss0) -# Partial Jacobians -# J_ir = interest_rates.jacobian(ss, ['r', 'tau']) -# J_ha = household.jacobian(ss, ['rpost', 'tau'], T=5) -# J1 = J_ha.compose(J_ir) - -# Let's make a simple combined block -# hh = sj.combine([interest_rates, household], name='HH') -# J2 = hh.jacobian(ss, exogenous=['r', 'tau'], T=4) - - -'''Test H_U''' - -unknowns = ['Y'] -targets = ['asset_mkt'] -exogenous = ['r'] -T = 5 - -# J = dag.partial_jacobians(ss, inputs=unknowns + exogenous, T=5) - -# HZ1 = dag.jacobian(ss, exogenous=exogenous, outputs=targets, T=5) -# HZ2 = get_H_U(dag.blocks, exogenous, targets, T, ss=ss) -# np.all(HZ1['asset_mkt']['r'] == HZ2) -# -# HU1 = dag.jacobian(ss, exogenous=unknowns, outputs=targets, T=5) -# HU2 = get_H_U(dag.blocks, unknowns, targets, T, ss=ss) -# np.all(HU1 == HU2) - -# J_int = interest_rates.jacobian(ss, exogenous=['r']) -# J_hh = household.jacobian(ss, exogenous=['Y', 'rpost'], T=4) -# -# # This should have -# J_all1 = J_hh.compose(J_int) -# J_all2 = J_int.compose(J_hh) - - -# G = dag.solve_jacobian(ss0, inputs=['r'], outputs=['A', 'Y', 'asset_mkt', 'goods_mkt'], unknowns=['Y', 'B'], targets=['asset_mkt', 'B_rule'], T=300) - -G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300) - -shock = ImpulseDict({'r': 0.001*0.9**np.arange(300)}) - -td1 = G @ shock +# Precompute household Jacobian +Js = {'household': household.jacobian(ss, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} +# Solve Jacobian +G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], + unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) +shock = ImpulseDict({'r': 1E-4*0.9**np.arange(300)}) +td_lin1 = G @ shock -td_lin = dag.solve_impulse_linear(ss, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) +# Compare to solve_impulse_linear +td_lin2 = dag.solve_impulse_linear(ss, unknowns=['Y'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) +assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) -assert all(np.allclose(td1[k], td_lin[k]) for k in td1) +# Solve impulse_nonlinear +td_hh = hh.impulse_nonlinear(ss, td_lin1[['Y']], outputs=['C', 'A'], Js=Js) -# td_nonlin = dag.solve_impulse_nonlinear(ss, {'r': 0.001*0.9**np.arange(300)}, -# unknowns=['B', 'Y'], targets=['asset_mkt', 'B_rule']) +td_nonlin = dag.solve_impulse_nonlinear(ss, unknowns=['Y'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index debbe3a..c2b28db 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -77,15 +77,18 @@ def _steady_state(self, calibration, dissolve=[], helper_blocks=None, **kwargs): return ss - def _impulse_nonlinear(self, ss, exogenous, **kwargs): - irf_nonlin_partial_eq = deepcopy(exogenous) + def _impulse_nonlinear(self, ss, inputs, outputs, Js): + original_outputs = outputs + outputs = (outputs | self._required) - ss._vector_valued() + + irf_nonlin_partial_eq = deepcopy(inputs) for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update({k: v for k, v in block.impulse_nonlinear(ss, input_args, **kwargs)}) + irf_nonlin_partial_eq.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, Js)) - return ImpulseDict(irf_nonlin_partial_eq) + return irf_nonlin_partial_eq[original_outputs] def _impulse_linear(self, ss, inputs, outputs, Js): original_outputs = outputs @@ -98,7 +101,7 @@ def _impulse_linear(self, ss, inputs, outputs, Js): if input_args: # If this block is actually perturbed irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js)) - return ImpulseDict(irf_lin_partial_eq[original_outputs]) + return irf_lin_partial_eq[original_outputs] def _partial_jacobians(self, ss, inputs, outputs, T, Js): # Add intermediate inputs; remove vector-valued inputs diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 1abf725..357c6e8 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -1,4 +1,3 @@ -import warnings import copy import numpy as np @@ -7,9 +6,8 @@ from ..primitives import Block from .. import utilities as utils from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict, verify_saved_jacobian +from ..jacobian.classes import JacobianDict from .support.bijection import Bijection -from ..devtools.deprecate import rename_output_list_to_outputs def het(exogenous, policy, backward, backward_init=None): @@ -236,20 +234,15 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, {self.name: {k: ss[k] for k in ss if k in self.internal}}) - def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=False, grid_paths=None): - """Evaluate transitional dynamics for HetBlock given dynamic paths for inputs in kwargs, - assuming that we start and end in steady state ss, and that all inputs not specified in - kwargs are constant at their ss values. Analog to SimpleBlock.td. + def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnindividual=False, grid_paths=None): + """Evaluate transitional dynamics for HetBlock given dynamic paths for `inputs`, + assuming that we start and end in steady state `ss`, and that all inputs not specified in + `inputs` are constant at their ss values. - CANNOT provide time-varying paths of grid or Markov transition matrix for now. + CANNOT provide time-varying Markov transition matrix for now. - Parameters - ---------- - ss : SteadyStateDict - all steady-state info, intended to be from .ss() - exogenous : dict of {str : array(T, ...)} - all time-varying inputs here (in deviations), with first dimension being time - this must have same length T for all entries (all outputs will be calculated up to T) + Special inputs + -------------- monotonic : [optional] bool flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us to use faster interpolation routines, otherwise use slower robust to nonmonotonicity @@ -257,20 +250,13 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies - - Returns - ---------- - td : dict - if returnindividual = False, time paths for aggregates (uppercase) for all outputs - of self.back_step_fun except self.back_iter_vars - if returnindividual = True, additionally time paths for distribution and for all outputs - of self.back_Step_fun on the full grid """ - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] + # # infer T from exogenous, check that all shocks have same length + # shock_lengths = [x.shape[0] for x in exogenous.values()] + # if shock_lengths[1:] != shock_lengths[:-1]: + # raise ValueError('Not all shocks in kwargs (exogenous) are same length!') + # T = shock_lengths[0] + T = inputs.T # copy from ss info Pi_T = ss.internal[self.name][self.exogenous].T.copy() @@ -285,7 +271,7 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa grid[k] = grid_paths[k] use_ss_grid[k] = False else: - grid[k] = ss[k+"_grid"] + grid[k] = ss[k + "_grid"] use_ss_grid[k] = True # allocate empty arrays to store result, assume all like D @@ -297,7 +283,7 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa backdict.update(copy.deepcopy(ss.internal[self.name])) for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) individual = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**self.make_inputs(backdict)))} backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -341,11 +327,12 @@ def _impulse_nonlinear(self, ss, exogenous, monotonic=False, returnindividual=Fa aggregate_hetoutputs = {} # return either this, or also include distributional information + # TODO: rethink this if returnindividual: return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, 'D': D_path}) - ss else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss + return ImpulseDict({**aggregates, **aggregate_hetoutputs})[outputs] - ss def _impulse_linear(self, ss, inputs, outputs, Js): @@ -359,7 +346,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): Parameters ---------- ss : dict, - all steady-state info, intended to be from .ss() + all steady-state info, intended to be from .steady_state() T : [optional] int number of time periods for T*T Jacobian exogenous : list of str @@ -369,8 +356,6 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): self.back_step_fun except self.back_iter_vars h : [optional] float h for numerical differentiation of backward iteration - Js : [optional] dict of {str: JacobianDict}} - supply saved Jacobians Returns ------- diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 2d51f73..13181cd 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -48,9 +48,9 @@ def _steady_state(self, ss): outputs = self.f.wrapped_call(ss, preprocess=ignore, postprocess=misc.numeric_primitive) return SteadyStateDict({**ss, **outputs}) - def _impulse_nonlinear(self, ss, exogenous): + def _impulse_nonlinear(self, ss, inputs, outputs, Js): input_args = {} - for k, v in exogenous.items(): + for k, v in inputs.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) @@ -59,7 +59,7 @@ def _impulse_nonlinear(self, ss, exogenous): if k not in input_args: input_args[k] = ignore(ss[k]) - return ImpulseDict(make_impulse_uniform_length(self.f(input_args))) - ss + return ImpulseDict(make_impulse_uniform_length(self.f(input_args)))[outputs] - ss def _impulse_linear(self, ss, inputs, outputs, Js): return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) @@ -97,6 +97,7 @@ def compute_single_shock_J(self, ss, i): return J +# TODO: move this to impulse.py? def make_impulse_uniform_length(out): T = np.max([np.size(v) for v in out.values()]) return {k: (np.full(T, misc.numeric_primitive(v)) if np.isscalar(v) else misc.numeric_primitive(v)) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 9eb81c8..399b958 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -1,13 +1,10 @@ -import warnings - from sequence_jacobian.utilities.ordered_set import OrderedSet from ..primitives import Block from ..blocks.simple_block import simple from ..blocks.parent import Parent -from ..utilities import graph -from ..jacobian.classes import JacobianDict, FactoredJacobianDict +from ..jacobian.classes import FactoredJacobianDict def solved(unknowns, targets, solver=None, solver_kwargs={}, name=""): @@ -73,14 +70,18 @@ def _steady_state(self, calibration, dissolve=[], unknowns=None, solver=None, tt return self.block.solve_steady_state(calibration, unknowns, self.targets, solver=solver, ttol=ttol, ctol=ctol, verbose=verbose) - def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): - return self.block.solve_impulse_nonlinear(ss, exogenous=exogenous, - unknowns=list(self.unknowns.keys()), Js=Js, - targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) + # def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): + # return self.block.solve_impulse_nonlinear(ss, exogenous=exogenous, + # unknowns=list(self.unknowns.keys()), Js=Js, + # targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), + # monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) + def _impulse_nonlinear(self, ss, inputs, outputs, Js): + return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), + inputs, outputs - self.unknowns.keys(), Js) def _impulse_linear(self, ss, inputs, outputs, Js): - return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), inputs, outputs - self.unknowns.keys(), Js) + return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), + inputs, outputs - self.unknowns.keys(), Js) def _jacobian(self, ss, inputs, outputs, T, Js): return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/jacobian/classes.py index 03f6384..77aefcd 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/jacobian/classes.py @@ -422,6 +422,7 @@ def __bool__(self): return bool(self.outputs) and bool(self.inputs) def compose(self, J): + """Returns self @ J""" if self.T is not None and J.T is not None and self.T != J.T: raise ValueError(f'Trying to multiply JacobianDicts with inconsistent dimensions {self.T} and {J.T}') @@ -446,6 +447,7 @@ def compose(self, J): return JacobianDict(J_oi, o_list, i_list) def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): + """Returns J @ x""" x = ImpulseDict(x) inputs = x.keys() & set(self.inputs) @@ -507,7 +509,8 @@ def __repr__(self): # TODO: test this def to_jacobian_dict(self): - return JacobianDict.unpack(-factored_solve(self.H_U_factored, np.eye(self.T*len(self.unknowns))), self.unknowns, self.targets, self.T) + return JacobianDict.unpack(-factored_solve(self.H_U_factored, np.eye(self.T*len(self.unknowns))), + self.unknowns, self.targets, self.T) def __matmul__(self, x): if isinstance(x, JacobianDict): @@ -530,28 +533,16 @@ def remap(self, x: Bijection): return newself def compose(self, J: JacobianDict): - # take intersection of J outputs with self.targets - # then pack that reduced J into a matrix, then apply lu_solve, then unpack + """Returns = -H_U^{-1} @ J""" Jsub = J[[o for o in self.targets if o in J.outputs]].pack(self.T) - X = -factored_solve(self.H_U_factored, Jsub) - return JacobianDict.unpack(X, self.unknowns, J.inputs, self.T) + out = -factored_solve(self.H_U_factored, Jsub) + return JacobianDict.unpack(out, self.unknowns, J.inputs, self.T) def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): - x = ImpulseDict(x)[self.targets] - - inputs = x.keys() & set(self.targets) - #J_oi = self.complete().nesteddict - # y = {} - - # for o in self.outputs: - # y[o] = np.zeros(x.T) - # for i in inputs: - # y[o] += J_oi[o][i] @ x[i] - - # return ImpulseDict(y, T=x.T) - # take intersection of x entries with self.targets - # then pack (should be ImpulseDict?) into a vector, then apply lu_solve, then unpack - pass + """Returns -H_U^{-1} @ x""" + xsub = ImpulseDict(x)[self.targets].pack() + out = -factored_solve(self.H_U_factored, xsub) + return ImpulseDict.unpack(out, self.unknowns, self.T) def ensure_valid_jacobiandict(d): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 617e71b..6d7b1e6 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -11,10 +11,8 @@ from .steady_state.drivers import steady_state as ss from .steady_state.support import provide_solver_default -# from .nonlinear import td_solve -# from .jacobian.drivers import get_impulse, get_G from .steady_state.classes import SteadyStateDict, UserProvidedSS -from .jacobian.classes import JacobianDict +from .jacobian.classes import JacobianDict, FactoredJacobianDict from .blocks.support.impulse import ImpulseDict from .blocks.support.bijection import Bijection from .blocks.parent import Parent @@ -90,22 +88,27 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) - def impulse_nonlinear(self, ss: SteadyStateDict, - exogenous: Dict[str, Array], **kwargs) -> ImpulseDict: - """Calculate a partial equilibrium, non-linear impulse response to a set of `exogenous` shocks - from a steady state `ss`.""" - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ exogenous, **kwargs) + def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], + outputs: Optional[List[str]] = None, + Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> ImpulseDict: + # TODO: do we want Js at all? better alternative to kwargs? + """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` + around a steady state `ss`.""" + inputs = ImpulseDict(inputs) + _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **kwargs) def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], - outputs: Optional[List[str]] = None, Js={}) -> ImpulseDict: + outputs: Optional[List[str]] = None, + Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: """Calculate a partial equilibrium, linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" inputs = ImpulseDict(inputs) _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js) - def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): - # TODO: annotate signature + def partial_jacobians(self, ss: SteadyStateDict, inputs: List[str] = None, outputs: List[str] = None, + T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = {}): if inputs is None: inputs = self.inputs if outputs is None: @@ -129,7 +132,7 @@ def partial_jacobians(self, ss, inputs=None, outputs=None, T=None, Js={}): return partial def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, - T: Optional[int] = None, Js={}) -> JacobianDict: + T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = {}) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) @@ -159,38 +162,80 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], solver = solver if solver else provide_solver_default(unknowns) return ss(blocks, calibration, unknowns, targets, solver=solver, **kwargs) - def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], - unknowns: List[str], targets: List[str], + # def solve_impulse_nonlinear(self, ss: Dict[str, Union[Real, Array]], + # exogenous: Dict[str, Array], + # unknowns: List[str], targets: List[str], + # Js: Optional[Dict[str, JacobianDict]] = {}, + # **kwargs) -> ImpulseDict: + # """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks + # from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + # variables to be solved for and the target conditions that must hold in general equilibrium""" + # blocks = self.blocks if hasattr(self, "blocks") else [self] + # irf_nonlin_gen_eq = td_solve(blocks, ss, + # exogenous={k: v for k, v in exogenous.items()}, + # unknowns=unknowns, targets=targets, Js=Js, **kwargs) + # return ImpulseDict(irf_nonlin_gen_eq) + def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], + inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]], Js: Optional[Dict[str, JacobianDict]] = {}, - **kwargs) -> ImpulseDict: - """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks - from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] - irf_nonlin_gen_eq = td_solve(blocks, ss, - exogenous={k: v for k, v in exogenous.items()}, - unknowns=unknowns, targets=targets, Js=Js, **kwargs) - return ImpulseDict(irf_nonlin_gen_eq) + tol: Optional[Real] = 1E-8, maxit: Optional[int] = 30, + verbose: Optional[bool] = True) -> ImpulseDict: + """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` + around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the `targets` that must hold in general equilibrium""" + inputs = ImpulseDict(inputs) + input_names, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + T = inputs.T + + # initialize guess for unknowns to steady state + U = ImpulseDict({k: np.zeros(T) for k in unknowns}) + + # obtain Jacobian of targets wrt to unknowns + Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js) + H_U = self.jacobian(ss, unknowns, targets, T, Js) + H_U_factored = FactoredJacobianDict(H_U, T) + + # iterate until convergence + for it in range(maxit): + # results = td_map(block_list, ss, exogenous, unknown_paths, sort=sort, + # monotonic=monotonic, returnindividual=returnindividual, + # grid_paths=grid_paths) + results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js=Js) + errors = {k: np.max(np.abs(results[k])) for k in targets} + if verbose: + print(f'On iteration {it}') + for k in errors: + print(f' max error for {k} is {errors[k]:.2E}') + if all(v < tol for v in errors.values()): + break + else: + # update guess U by -H_U^(-1) times errors + U += H_U_factored.apply(results) + else: + raise ValueError(f'No convergence after {maxit} backward iterations!') + + return results | U def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]], Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: - """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - variables to be solved for and the target conditions that must hold in general equilibrium""" + """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` + around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous + variables to be solved for and the target conditions that must hold in general equilibrium""" inputs = ImpulseDict(inputs) input_names, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + T = inputs.T - Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, inputs.T, Js) + Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js) - H_U = self.jacobian(ss, unknowns, targets, inputs.T, Js).pack(inputs.T) + H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) dH = self.impulse_linear(ss, inputs, targets, Js).pack() - dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, inputs.T) + dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) return self.impulse_linear(ss, dU | inputs, outputs, Js) - def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = {}) -> JacobianDict: From 874dbbc8314ef2481042d1f9079a143bf5694a62 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 24 Aug 2021 16:47:47 -0500 Subject: [PATCH 230/288] Add __array_finalize__ method to Displace to ensure that Displace objects that are created not through the explicit constructor still have the .ss and .name fields --- src/sequence_jacobian/blocks/support/simple_displacement.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sequence_jacobian/blocks/support/simple_displacement.py b/src/sequence_jacobian/blocks/support/simple_displacement.py index 87ee155..0ace468 100644 --- a/src/sequence_jacobian/blocks/support/simple_displacement.py +++ b/src/sequence_jacobian/blocks/support/simple_displacement.py @@ -282,6 +282,10 @@ def __new__(cls, x, ss=None, name='UNKNOWN'): obj.name = name return obj + def __array_finalize__(self, obj): + self.ss = getattr(obj, "ss", None) + self.name = getattr(obj, "name", "UNKNOWN") + def __repr__(self): return f'Displace({numeric_primitive(self)})' From 3a424154de5bf93899c2131ef198cd6fbecf02fd Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 24 Aug 2021 18:34:28 -0400 Subject: [PATCH 231/288] Major milestone! Got rid of jacobian/drivers.py and nonlinear.py --- src/sequence_jacobian/__init__.py | 4 +- .../blocks/combined_block.py | 8 +- src/sequence_jacobian/blocks/discont_block.py | 83 ++---- src/sequence_jacobian/blocks/het_block.py | 32 +-- src/sequence_jacobian/blocks/solved_block.py | 5 - .../blocks/support/impulse.py | 16 +- src/sequence_jacobian/jacobian/drivers.py | 261 ------------------ src/sequence_jacobian/nonlinear.py | 112 -------- src/sequence_jacobian/primitives.py | 28 +- tests/base/test_estimation.py | 2 +- tests/base/test_jacobian.py | 11 +- tests/base/test_simple_block.py | 8 +- tests/base/test_transitional_dynamics.py | 18 +- .../base/test_workflow.py | 84 +++--- 14 files changed, 99 insertions(+), 573 deletions(-) delete mode 100644 src/sequence_jacobian/jacobian/drivers.py delete mode 100644 src/sequence_jacobian/nonlinear.py rename nesting_test.py => tests/base/test_workflow.py (59%) diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index 97fbde7..72f8d84 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -1,6 +1,6 @@ """Public-facing objects.""" -from . import estimation, jacobian, nonlinear, utilities, devtools +from . import estimation, jacobian, utilities, devtools from .blocks.simple_block import simple from .blocks.het_block import het, hetoutput @@ -15,8 +15,6 @@ from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved from .steady_state.drivers import steady_state -from .jacobian.drivers import get_G, get_H_U, get_impulse -from .nonlinear import td_solve # Useful utilities for setting up HetBlocks from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index c2b28db..c2e970c 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -104,12 +104,10 @@ def _impulse_linear(self, ss, inputs, outputs, Js): return irf_lin_partial_eq[original_outputs] def _partial_jacobians(self, ss, inputs, outputs, T, Js): - # Add intermediate inputs; remove vector-valued inputs vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued - # Compute Jacobians along the DAG curlyJs = {} for block in self.blocks: descendants = block.descendants if isinstance(block, Parent) else {block.name: None} @@ -121,19 +119,15 @@ def _partial_jacobians(self, ss, inputs, outputs, T, Js): return curlyJs def _jacobian(self, ss, inputs, outputs, T, Js={}): - # _partial_jacobians should calculate partial jacobians with exactly the inputs and outputs we want Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) original_outputs = outputs total_Js = JacobianDict.identity(inputs) - # horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! + # TODO: horrible, redoing work from partial_jacobians, also need more efficient sifting of intermediates! vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued - - # Forward accumulate individual Jacobians - # ANNOYINGLY PARALLEL TO PARTIAL_JACOBIANS! for block in self.blocks: descendants = block.descendants if isinstance(block, Parent) else {block.name: None} Js_block = {k: v for k, v in Js.items() if k in descendants} diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py index a35d237..3ea0d3b 100644 --- a/src/sequence_jacobian/blocks/discont_block.py +++ b/src/sequence_jacobian/blocks/discont_block.py @@ -1,4 +1,3 @@ -import warnings import copy import numpy as np @@ -207,7 +206,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars """ - ss = copy.deepcopy(calibration) + ss = copy.deepcopy(calibration.toplevel) # extract information from calibration grid = calibration[self.policy + '_grid'] @@ -233,20 +232,16 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregate_hetoutputs = {} ss.update({**hetoutputs, **aggregate_hetoutputs}) - return SteadyStateDict(ss, internal=self) + return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, + {self.name: {k: ss[k] for k in ss if k in self.internal}}) - def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=None): + def _impulse_nonlinear(self, ss, inputs, outputs, Js, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for DiscontBlock given dynamic paths for inputs in exogenous, assuming that we start and end in steady state ss, and that all inputs not specified in exogenous are constant at their ss values. Analog to SimpleBlock.td. - Parameters - ---------- - ss : SteadyStateDict - all steady-state info, intended to be from .ss() - exogenous : dict of {str : array(T, ...)} - all time-varying inputs here (in deviations), with first dimension being time - this must have same length T for all entries (all outputs will be calculated up to T) + Block-specific inputs + --------------------- returnindividual : [optional] bool return distribution and full outputs on grid grid_paths: [optional] dict of {str: array(T, Number of grid points)} @@ -260,14 +255,9 @@ def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=N if returnindividual = True, additionally time paths for distribution and for all outputs of self.back_Step_fun on the full grid """ - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - # copy from ss info - D, P = ss.internal[self.name]['D'], ss.internal[self.name][self.disc_policy] + T = inputs.T + D = ss.internal[self.name]['D'] + P = ss.internal[self.name][self.disc_policy] # construct grids for policy variables either from the steady state grid if the grid is meant to be # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. @@ -289,7 +279,7 @@ def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=N if self.hetinput is not None: indict = dict(ss.items()) for t in range(T): - indict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + indict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) hetout = dict(zip(self.hetinput_outputs_order, self.hetinput(**{k: indict[k] for k in self.hetinput_inputs}))) for k in self.hetinput_outputs_order: @@ -300,7 +290,7 @@ def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=N backdict.update(copy.deepcopy(ss.internal[self.name])) for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t, ...] for k, v in exogenous.items()}) + backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) # add in multidimensional inputs EXCEPT exogenous state transitions (at lead 0) backdict.update({k: ss.internal[self.name][k] + v[t, ...] for k, v in multidim_inputs.items() if k not in self.exogenous}) @@ -367,53 +357,18 @@ def _impulse_nonlinear(self, ss, exogenous, returnindividual=False, grid_paths=N else: return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - def _impulse_linear(self, ss, exogenous, T=None, Js=None, **kwargs): - # infer T from exogenous, check that all shocks have same length - shock_lengths = [x.shape[0] for x in exogenous.values()] - if shock_lengths[1:] != shock_lengths[:-1]: - raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - T = shock_lengths[0] - - return ImpulseDict(self.jacobian(ss, list(exogenous.keys()), T=T, Js=Js, **kwargs).apply(exogenous)) - - def _jacobian(self, ss, exogenous=None, T=300, outputs=None, Js=None, h=1E-4): - """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. + def _impulse_linear(self, ss, inputs, outputs, Js): + return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) - Parameters - ---------- - ss : dict, - all steady-state info, intended to be from .ss() - T : [optional] int - number of time periods for T*T Jacobian - exogenous : list of str - names of input variables to differentiate wrt (main cost scales with # of inputs) - outputs : list of str - names of output variables to get derivatives of, if not provided assume all outputs of - self.back_step_fun except self.back_iter_vars + def _jacobian(self, ss, inputs, outputs, T, h=1E-4): + """ + Block-specific inputs + --------------------- h : [optional] float h for numerical differentiation of backward iteration - Js : [optional] dict of {str: JacobianDict}} - supply saved Jacobians - - Returns - ------- - J : dict of {str: dict of {str: array(T,T)}} - J[o][i] for output o and input i gives T*T Jacobian of o with respect to i """ - # The default set of outputs are all outputs of the backward iteration function - # except for the backward iteration variables themselves - if exogenous is None: - exogenous = list(self.inputs) - if outputs is None: - outputs = self.non_back_iter_outputs - - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in exogenous] - - # if we supply Jacobians, use them if possible, warn if they cannot be used - if Js is not None: - outputs_cap = [o.capitalize() for o in outputs] - if verify_saved_jacobian(self.name, Js, outputs_cap, relevant_shocks, T): - return Js[self.name] + outputs = self.M_outputs.inv @ outputs # horrible + relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in inputs] # step 0: preliminary processing of steady state (ssin_dict, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space, D0, D2, Pi, P) = self.jac_prelim(ss) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 357c6e8..66e3939 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -241,8 +241,8 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind CANNOT provide time-varying Markov transition matrix for now. - Special inputs - -------------- + Block-specific inputs + --------------------- monotonic : [optional] bool flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us to use faster interpolation routines, otherwise use slower robust to nonmonotonicity @@ -251,14 +251,7 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies """ - # # infer T from exogenous, check that all shocks have same length - # shock_lengths = [x.shape[0] for x in exogenous.values()] - # if shock_lengths[1:] != shock_lengths[:-1]: - # raise ValueError('Not all shocks in kwargs (exogenous) are same length!') - # T = shock_lengths[0] T = inputs.T - - # copy from ss info Pi_T = ss.internal[self.name][self.exogenous].T.copy() D = ss.internal[self.name]['D'] @@ -341,26 +334,11 @@ def _impulse_linear(self, ss, inputs, outputs, Js): def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options - """Assemble nested dict of Jacobians of agg outputs vs. inputs, using fake news algorithm. - - Parameters - ---------- - ss : dict, - all steady-state info, intended to be from .steady_state() - T : [optional] int - number of time periods for T*T Jacobian - exogenous : list of str - names of input variables to differentiate wrt (main cost scales with # of inputs) - outputs : list of str - names of output variables to get derivatives of, if not provided assume all outputs of - self.back_step_fun except self.back_iter_vars + """ + Block-specific inputs + --------------------- h : [optional] float h for numerical differentiation of backward iteration - - Returns - ------- - J : dict of {str: dict of {str: array(T,T)}} - J[o][i] for output o and input i gives T*T Jacobian of o with respect to i """ outputs = self.M_outputs.inv @ outputs # horrible diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 399b958..e9ffa14 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -70,11 +70,6 @@ def _steady_state(self, calibration, dissolve=[], unknowns=None, solver=None, tt return self.block.solve_steady_state(calibration, unknowns, self.targets, solver=solver, ttol=ttol, ctol=ctol, verbose=verbose) - # def _impulse_nonlinear(self, ss, exogenous=None, monotonic=False, Js=None, returnindividual=False, verbose=False): - # return self.block.solve_impulse_nonlinear(ss, exogenous=exogenous, - # unknowns=list(self.unknowns.keys()), Js=Js, - # targets=self.targets if isinstance(self.targets, list) else list(self.targets.keys()), - # monotonic=monotonic, returnindividual=returnindividual, verbose=verbose) def _impulse_nonlinear(self, ss, inputs, outputs, Js): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), inputs, outputs - self.unknowns.keys(), Js) diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/blocks/support/impulse.py index ab93135..430e1b0 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/blocks/support/impulse.py @@ -56,10 +56,10 @@ def __getitem__(self, item): def __add__(self, other): if isinstance(other, (float, int)): return type(self)({k: v + other for k, v in self.impulse.items()}) - elif isinstance(other, SteadyStateDict): + elif isinstance(other, (SteadyStateDict, ImpulseDict)): return type(self)({k: v + other[k] for k, v in self.impulse.items()}) else: - NotImplementedError('Only a number or a SteadyStateDict can be added from an ImpulseDict.') + return NotImplementedError('Only a number or a SteadyStateDict can be added from an ImpulseDict.') def __sub__(self, other): if isinstance(other, (float, int)): @@ -67,15 +67,15 @@ def __sub__(self, other): elif isinstance(other, (SteadyStateDict, ImpulseDict)): return type(self)({k: v - other[k] for k, v in self.impulse.items()}) else: - NotImplementedError('Only a number or a SteadyStateDict can be subtracted from an ImpulseDict.') + return NotImplementedError('Only a number or a SteadyStateDict can be subtracted from an ImpulseDict.') def __mul__(self, other): if isinstance(other, (float, int)): return type(self)({k: v * other for k, v in self.impulse.items()}) - elif isinstance(other, SteadyStateDict): + elif isinstance(other, (SteadyStateDict, ImpulseDict)): return type(self)({k: v * other[k] for k, v in self.impulse.items()}) else: - NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') + return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __rmul__(self, other): if isinstance(other, (float, int)): @@ -83,7 +83,7 @@ def __rmul__(self, other): elif isinstance(other, SteadyStateDict): return type(self)({k: v * other[k] for k, v in self.impulse.items()}) else: - NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') + return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __truediv__(self, other): if isinstance(other, (float, int)): @@ -92,7 +92,7 @@ def __truediv__(self, other): elif isinstance(other, SteadyStateDict): return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) else: - NotImplementedError('An ImpulseDict can only be divided by a number or a SteadyStateDict.') + return NotImplementedError('An ImpulseDict can only be divided by a number or a SteadyStateDict.') def __matmul__(self, x): # remap keys in toplevel @@ -101,7 +101,7 @@ def __matmul__(self, x): new.impulse = x @ self.impulse return new else: - NotImplemented + return NotImplemented def __rmatmul__(self, x): return self.__matmul__(x) diff --git a/src/sequence_jacobian/jacobian/drivers.py b/src/sequence_jacobian/jacobian/drivers.py deleted file mode 100644 index afc0416..0000000 --- a/src/sequence_jacobian/jacobian/drivers.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Main methods (drivers) for computing and manipulating both block-level and model-level Jacobians""" - -import numpy as np - -from .classes import JacobianDict -from .support import pack_vectors, unpack_vectors -from ..utilities import misc, graph - -'''Drivers: - - get_H_U : get H_U matrix mapping all unknowns to all targets - - get_impulse : get single GE impulse response - - get_G : get G matrices characterizing all GE impulse responses - - - curlyJs_sorted : get block Jacobians curlyJ and return them topologically sorted - - forward_accumulate : forward accumulation on DAG, taking in topologically sorted Jacobians -''' - - -def get_H_U(blocks, unknowns, targets, T, ss=None, Js={}): - """Get T*n_u by T*n_u matrix H_U, Jacobian mapping all unknowns to all targets. - - Parameters - ---------- - blocks : list, simple blocks, het blocks, or jacdicts - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : int, truncation horizon - (if asymptotic, truncation horizon for backward iteration in HetBlocks) - ss : [optional] dict, steady state required if blocks contains any non-jacdicts - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - H_U : - if asymptotic=False: - array(T*n_u*T*n_u) H_U, Jacobian mapping all unknowns to all targets - is asymptotic=True: - array((2*Tpost-1)*n_u*n_u), representation of asymptotic columns of H_U - """ - - # do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns, ss, T, Js) - - # do matrix forward accumulation to get H_U = J^(curlyH, curlyU) - H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) - - # pack these n_u^2 matrices, each T*T, into a single matrix - return H_U_unpacked[targets, unknowns].pack(T) - - -def get_impulse(blocks, dZ, unknowns, targets, T=None, ss=None, outputs=None, Js={}): - """Get a single general equilibrium impulse response. - - Extremely fast when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed - and supplied to this function. Less so but still faster when H_U already computed. - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - dZ : dict, path of an exogenous variable - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if blocks contains non-jacdicts - outputs : [optional] list of str, variables we want impulse responses for - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - out : dict, impulse responses to shock dZ - """ - # step 0 (preliminaries): infer T, do topological sort and get curlyJs - if T is None: - for x in dZ.values(): - T = len(x) - break - - curlyJs, required = curlyJ_sorted(blocks, unknowns + list(dZ.keys()), ss, T, Js) - - # step 1: do (matrix) forward accumulation to get H_U = J^(curlyH, curlyU) - H_U_unpacked = forward_accumulate(curlyJs, unknowns, targets, required) - - # step 2: do (vector) forward accumulation to get J^(o, curlyZ)dZ for all o in - # 'alloutputs', the combination of outputs (if specified) and targets - alloutputs = None - if outputs is not None: - alloutputs = set(outputs) | set(targets) - - J_curlyZ_dZ = forward_accumulate(curlyJs, dZ, alloutputs, required) - - # step 3: solve H_UdU = -H_ZdZ for dU - H_U = H_U_unpacked[targets, unknowns].pack(T) - H_ZdZ_packed = pack_vectors(J_curlyZ_dZ, targets, T) - dU_packed = -np.linalg.solve(H_U, H_ZdZ_packed) - dU = unpack_vectors(dU_packed, unknowns, T) - - # step 4: do (vector) forward accumulation to get J^(o, curlyU)dU - # then sum together with J^(o, curlyZ)dZ to get all output impulse responses - J_curlyU_dU = forward_accumulate(curlyJs, dU, outputs, required) - if outputs is None: - outputs = J_curlyZ_dZ.keys() | J_curlyU_dU.keys() - return {**dZ, **{o: J_curlyZ_dZ.get(o, np.zeros(T)) + J_curlyU_dU.get(o, np.zeros(T)) for o in outputs}} - - -def get_G(blocks, exogenous, unknowns, targets, T=300, ss=None, outputs=None, Js={}): - """Compute Jacobians G that fully characterize general equilibrium outputs in response - to all exogenous shocks in 'exogenous' - - Faster when H_U_factored = utils.misc.factor(get_HU(...)) has already been computed - and supplied to this function. Less so but still faster when H_U already computed. - Relative benefit of precomputing these not as extreme as for get_impulse, since - obtaining and solving with H_U is a less dominant component of cost for getting Gs. - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - exogenous : list of str, names of exogenous shocks in DAG - unknowns : list of str, names of unknowns in DAG - targets : list of str, names of targets in DAG - T : [optional] int, truncation horizon - ss : [optional] dict, steady state required if blocks contains non-jacdicts - outputs : [optional] list of str, variables we want impulse responses for - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - G : dict of dict, Jacobians for general equilibrium mapping from exogenous to outputs - """ - - # step 1: do topological sort and get curlyJs - curlyJs, required = curlyJ_sorted(blocks, unknowns + exogenous, ss, T, Js) - - # step 2: do (matrix) forward accumulation to get - # H_U = J^(curlyH, curlyU) [if not provided], H_Z = J^(curlyH, curlyZ) - J_curlyH_U = forward_accumulate(curlyJs, unknowns, targets, required) - J_curlyH_Z = forward_accumulate(curlyJs, exogenous, targets, required) - - # step 3: solve for G^U, unpack - H_U = J_curlyH_U[targets, unknowns].pack(T) - H_Z = J_curlyH_Z[targets, exogenous].pack(T) - - G_U = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, exogenous, T) - - # step 4: forward accumulation to get all outputs starting with G_U - # by default, don't calculate targets! - curlyJs = [G_U] + curlyJs - if outputs is None: - outputs = set().union(*(curlyJ.outputs for curlyJ in curlyJs)) - set(targets) - return forward_accumulate(curlyJs, exogenous, outputs, required | set(unknowns)) - - -def curlyJ_sorted(blocks, inputs, ss=None, T=None, Js={}): - """ - Sort blocks along DAG and calculate their Jacobians (if not already provided) with respect to inputs - and with respect to outputs of other blocks - - Parameters - ---------- - blocks : list, simple blocks or jacdicts - inputs : list, input names we need to differentiate with respect to - ss : [optional] dict, steady state, needed if blocks includes blocks themselves - T : [optional] int, horizon for differentiation, needed if blocks includes hetblock itself - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - - Returns - ------- - curlyJs : list of dict of dict, curlyJ for each block in order of topological sort - required : list, outputs of some blocks that are needed as inputs by others - """ - - # step 1: get topological sort and required - topsorted = graph.block_sort(blocks) - required = graph.find_outputs_that_are_intermediate_inputs(blocks) - - # Remove any vector-valued outputs that are intermediate inputs, since we don't want - # to compute Jacobians with respect to vector-valued variables - if ss is not None: - vv_vars = set([k for k, v in ss.items() if np.size(v) > 1]) - required -= vv_vars - - # step 2: compute Jacobians and put them in right order - curlyJs = [] - shocks = set(inputs) | required - for num in topsorted: - block = blocks[num] - jac = block.jacobian(ss, inputs=list(shocks), Js=Js, **{k: v for k, v in {"T": T}.items() - if k in misc.input_kwarg_list(block.jacobian)}) - - # If the returned Jacobian is empty (i.e. the shocks do not affect any outputs from the block) - # then don't add it to the list of curlyJs to be returned - if not jac: - continue - else: - curlyJs.append(JacobianDict(jac)) - - return curlyJs, required - - -def forward_accumulate(curlyJs, inputs, outputs=None, required=None): - """ - Use forward accumulation on topologically sorted Jacobians in curlyJs to get - all cumulative Jacobians with respect to 'inputs' if inputs is a list of names, - or get outcome of apply to 'inputs' if inputs is dict. - - Optionally only find outputs in 'outputs', especially if we have knowledge of - what is required for later Jacobians. - - Note that the overloading of @ means that this works automatically whether curlyJs are ordinary - matrices, simple_block.SimpleSparse objects, or asymptotic.AsymptoticTimeInvariant objects, - as long as the first and third are not mixed (since multiplication not defined for them). - - Much-extended version of chain_jacobians. - - Parameters - ---------- - curlyJs : list of dict of dict, curlyJ for each block in order of topological sort - inputs : list or dict, input names to differentiate with respect to, OR dict of input vectors - outputs : [optional] list or set, outputs we're interested in - required : [optional] list or set, outputs needed for later curlyJs (only useful w/outputs) - - Returns - ------- - out : dict of dict or dict, either total J for each output wrt all inputs or - outcome from applying all curlyJs - """ - - if outputs is not None and required is not None: - # if list of outputs provided, we need to obtain these and 'required' along the way - alloutputs = set(outputs) | set(required) - else: - # otherwise, set to None, implies default behavior of obtaining all outputs in curlyJs - alloutputs = None - - # if inputs is list (jacflag=True), interpret as list of inputs for which we want to calculate jacs - # if inputs is dict, interpret as input *paths* to which we apply all Jacobians in curlyJs - jacflag = not isinstance(inputs, dict) - - if jacflag: - # Jacobians of inputs with respect to themselves are the identity, initialize with this - # out = {i: {i: utils.special_matrices.IdentityMatrix()} for i in inputs} - out = JacobianDict.identity(inputs) - else: - out = inputs.copy() - - # iterate through curlyJs, in what is presumed to be a topologically sorted order - for curlyJ in curlyJs: - curlyJ = JacobianDict(curlyJ).complete() - if alloutputs is not None: - # if we want specific list of outputs, restrict curlyJ to that before continuing - curlyJ = curlyJ[[k for k in alloutputs if k in curlyJ.outputs]] - if jacflag: - out.update(curlyJ.compose(out)) - else: - out.update(curlyJ.apply(out)) - - if outputs is not None: - # if we want specific list of outputs, restrict to that - # (dropping 'required' in 'alloutputs' that was needed for intermediate computations) - return out[[k for k in outputs if k in out.outputs]] - else: - return out diff --git a/src/sequence_jacobian/nonlinear.py b/src/sequence_jacobian/nonlinear.py deleted file mode 100644 index 3661e6e..0000000 --- a/src/sequence_jacobian/nonlinear.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Functions for solving for the non-linear transition dynamics provided a given shock path (e.g. solving MIT shocks)""" - -import numpy as np - -from .utilities import misc, graph -from .jacobian.drivers import get_H_U -from .jacobian.support import pack_vectors, unpack_vectors - - -def td_solve(block_list, ss, exogenous, unknowns, targets, Js={}, monotonic=False, - returnindividual=False, tol=1E-8, maxit=30, verbose=True, grid_paths=None): - """Solves for GE nonlinear perfect foresight paths for SHADE model, given shocks in kwargs. - - Use a quasi-Newton method with the Jacobian H_U mapping unknowns to targets around steady state. - - Parameters - ---------- - block_list : list, blocks in model (SimpleBlocks or HetBlocks) - ss : dict, all steady-state information - exogenous : dict, all shocked Z go here, must all have same length T - unknowns : list, unknowns of SHADE DAG, the 'U' in H(U, Z) - targets : list, targets of SHADE DAG, the 'H' in H(U, Z) - Js : [optional] dict of {str: JacobianDict}}, supply saved Jacobians - monotonic : [optional] bool, flag indicating HetBlock policy for some k' is monotonic in state k - (allows more efficient interpolation) - returnindividual: [optional] bool, flag to return individual outcomes from HetBlock.td - tol : [optional] scalar, for convergence of Newton's method we require |H| ImpulseDict: - # """Calculate a general equilibrium, non-linear impulse response to a set of `exogenous` shocks - # from a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous - # variables to be solved for and the target conditions that must hold in general equilibrium""" - # blocks = self.blocks if hasattr(self, "blocks") else [self] - # irf_nonlin_gen_eq = td_solve(blocks, ss, - # exogenous={k: v for k, v in exogenous.items()}, - # unknowns=unknowns, targets=targets, Js=Js, **kwargs) - # return ImpulseDict(irf_nonlin_gen_eq) def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], - inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]], + inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js: Optional[Dict[str, JacobianDict]] = {}, tol: Optional[Real] = 1E-8, maxit: Optional[int] = 30, verbose: Optional[bool] = True) -> ImpulseDict: @@ -188,19 +175,13 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) T = inputs.T - # initialize guess for unknowns to steady state - U = ImpulseDict({k: np.zeros(T) for k in unknowns}) - - # obtain Jacobian of targets wrt to unknowns Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js) H_U = self.jacobian(ss, unknowns, targets, T, Js) H_U_factored = FactoredJacobianDict(H_U, T) - # iterate until convergence + # Newton's method + U = ImpulseDict({k: np.zeros(T) for k in unknowns}) for it in range(maxit): - # results = td_map(block_list, ss, exogenous, unknown_paths, sort=sort, - # monotonic=monotonic, returnindividual=returnindividual, - # grid_paths=grid_paths) results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js=Js) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: @@ -210,7 +191,6 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ if all(v < tol for v in errors.values()): break else: - # update guess U by -H_U^(-1) times errors U += H_U_factored.apply(results) else: raise ValueError(f'No convergence after {maxit} backward iterations!') diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index e485c49..97b5123 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -3,7 +3,7 @@ import pytest import numpy as np -from sequence_jacobian import get_G, estimation +from sequence_jacobian import estimation # See test_determinacy.py for the to-do describing this suppression diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 24c2714..95813cc 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -2,7 +2,6 @@ import numpy as np -from sequence_jacobian.jacobian.drivers import get_G, forward_accumulate, curlyJ_sorted from sequence_jacobian.jacobian.classes import JacobianDict @@ -123,9 +122,9 @@ def test_fake_news_v_actual(one_asset_hank_dag): def test_fake_news_v_direct_method(one_asset_hank_dag): - hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag + hank_model, _, _, _, ss = one_asset_hank_dag - household = hank_model._blocks_unsorted[0] + household = hank_model['household'] T = 40 exogenous = ['r'] output_list = household.non_back_iter_outputs @@ -138,15 +137,15 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): # (better than subtracting by ss since ss not exact) # monotonic=True lets us know there is monotonicity of policy rule, makes TD run faster # .impulse_nonlinear requires at least one input 'shock', so we put in steady-state w - td_noshock = household.impulse_nonlinear(ss, exogenous={'w': np.zeros(T)}, monotonic=True) + td_noshock = household.impulse_nonlinear(ss, {'w': np.zeros(T)}) for i in exogenous: # simulate with respect to a shock at each date up to T for t in range(T): - td_out = household.impulse_nonlinear(ss, exogenous={i: h * (np.arange(T) == t)}) + td_out = household.impulse_nonlinear(ss, {i: h * (np.arange(T) == t)}) # store results as column t of J[o][i] for each outcome o for o in output_list: Js_direct[o.capitalize()][i][:, t] = (td_out[o.capitalize()] - td_noshock[o.capitalize()]) / h - assert np.linalg.norm(Js["C"]["r"] - Js_direct["C"]["r"], np.inf) < 3e-4 + assert np.linalg.norm(Js['C']['r'] - Js_direct['C']['r'], np.inf) < 3e-4 diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 721f969..ac18a50 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -41,8 +41,8 @@ def test_block_consistency(block, ss): ss_results = block.steady_state(ss) # now if we put in constant inputs, td should give us the same! - td_results = block.impulse_nonlinear(ss_results, exogenous={k: np.zeros(20) for k in ss.keys()}) - for k, v in td_results.impulse.items(): + td_results = block.impulse_nonlinear(ss_results, {k: np.zeros(20) for k in ss.keys()}) + for v in td_results.impulse.values(): assert np.all(v == 0) # now get the Jacobian @@ -54,8 +54,8 @@ def test_block_consistency(block, ss): h = 1E-5 all_shocks = {i: np.random.rand(10) for i in block.inputs} - td_up = block.impulse_nonlinear(ss_results, exogenous={i: h*shock for i, shock in all_shocks.items()}) - td_dn = block.impulse_nonlinear(ss_results, exogenous={i: -h*shock for i, shock in all_shocks.items()}) + td_up = block.impulse_nonlinear(ss_results, {i: h*shock for i, shock in all_shocks.items()}) + td_dn = block.impulse_nonlinear(ss_results, {i: -h*shock for i, shock in all_shocks.items()}) linear_impulses = {o: (td_up.impulse[o] - td_dn.impulse[o])/(2*h) for o in td_up.impulse} linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up.impulse} diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 72f332a..a3386c6 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -19,8 +19,8 @@ def test_rbc_td(rbc_dag): dZ[:, 1] = np.concatenate((np.zeros(news), dZ[:-news, 0])) dC = 100 * G['C']['Z'] @ dZ / ss['C'] - td_nonlin = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 0]}, unknowns=unknowns, targets=targets) - td_nonlin_news = rbc_model.solve_impulse_nonlinear(ss, {"Z": dZ[:, 1]}, unknowns=unknowns, targets=targets) + td_nonlin = rbc_model.solve_impulse_nonlinear(ss, unknowns, targets, inputs={"Z": dZ[:, 0]}, outputs=['C']) + td_nonlin_news = rbc_model.solve_impulse_nonlinear(ss, unknowns, targets, inputs={"Z": dZ[:, 1]}, outputs=['C']) dC_nonlin = 100 * td_nonlin['C'] / ss['C'] dC_nonlin_news = 100 * td_nonlin_news['C'] / ss['C'] @@ -38,8 +38,7 @@ def test_ks_td(krusell_smith_dag): for shock_size, tol in [(0.01, 7e-3), (0.1, 0.6)]: dZ = shock_size * 0.8 ** np.arange(T) - td_nonlin = ks_model.solve_impulse_nonlinear(ss, {"Z": dZ}, unknowns=unknowns, targets=targets, - monotonic=True) + td_nonlin = ks_model.solve_impulse_nonlinear(ss, unknowns, targets, {"Z": dZ}) dr_nonlin = 10000 * td_nonlin['r'] dr_lin = 10000 * G['r']['Z'] @ dZ @@ -57,7 +56,7 @@ def test_hank_td(one_asset_hank_dag): rho_r, sig_r = 0.61, -0.01/4 drstar = sig_r * rho_r ** (np.arange(T)) - td_nonlin = hank_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, Js={'household': J_ha}) + td_nonlin = hank_model.solve_impulse_nonlinear(ss, unknowns, targets, {"rstar": drstar}, Js={'household': J_ha}) dC_nonlin = 100 * td_nonlin['C'] / ss['C'] dC_lin = 100 * G['C']['rstar'] @ drstar / ss['C'] @@ -65,6 +64,7 @@ def test_hank_td(one_asset_hank_dag): assert np.linalg.norm(dC_nonlin - dC_lin, np.inf) < 3e-3 +# TODO: needs to compute Jacobian of hetoutput `Chi` def test_two_asset_td(two_asset_hank_dag): two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag @@ -76,7 +76,7 @@ def test_two_asset_td(two_asset_hank_dag): for shock_size, tol in [(0.1, 3e-4), (1, 2e-2)]: drstar = shock_size * -0.0025 * 0.6 ** np.arange(T) - td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, unknowns, targets, {"rstar": drstar}, Js={'household': J_ha}) dY_nonlin = 100 * td_nonlin['Y'] @@ -106,14 +106,14 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): drstar = -0.0025 * 0.6 ** np.arange(T) dY = 100 * G['Y']['rstar'] @ drstar - td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, {"rstar": drstar}, unknowns, targets, + td_nonlin = two_asset_model.solve_impulse_nonlinear(ss, unknowns, targets, {"rstar": drstar}, Js={'household': J_ha}) dY_nonlin = 100 * (td_nonlin['Y'] - 1) dY_simple = 100 * G_simple['Y']['rstar'] @ drstar - td_nonlin_simple = two_asset_model_simple.solve_impulse_nonlinear(ss, {"rstar": drstar}, + td_nonlin_simple = two_asset_model_simple.solve_impulse_nonlinear(ss, unknowns_simple, targets_simple, - Js={'household': J_ha}) + {"rstar": drstar}, Js={'household': J_ha}) dY_nonlin_simple = 100 * (td_nonlin_simple['Y'] - 1) diff --git a/nesting_test.py b/tests/base/test_workflow.py similarity index 59% rename from nesting_test.py rename to tests/base/test_workflow.py index 08a428c..e7abbe8 100644 --- a/nesting_test.py +++ b/tests/base/test_workflow.py @@ -1,7 +1,6 @@ import numpy as np import sequence_jacobian as sj -from sequence_jacobian import het, simple, hetoutput, combine, solved, create_model, get_H_U -from sequence_jacobian.jacobian.classes import JacobianDict, ZeroMatrix +from sequence_jacobian import het, simple, hetoutput, solved, combine, create_model from sequence_jacobian.blocks.support.impulse import ImpulseDict @@ -107,18 +106,18 @@ def interest_rates(r): return rpost, rb -@simple -def fiscal(B, G, rb, Y, transfer): - rev = rb * B + G + transfer # revenue to be raised - tau = rev / Y - return rev, tau - # @simple -# def fiscal(B, G, rb, Y, transfer, rho_B): -# B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B -# rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised +# def fiscal(B, G, rb, Y, transfer): +# rev = rb * B + G + transfer # revenue to be raised # tau = rev / Y -# return B_rule, rev, tau +# return rev, tau + +@simple +def fiscal(B, G, rb, Y, transfer, rho_B): + B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / Y + return B_rule, rev, tau @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') @@ -136,41 +135,42 @@ def mkt_clearing(A, B, C, Y, G): return asset_mkt, goods_mkt -'''Try this''' - -# DAG with a nested CombinedBlock -hh = combine([household, income_state_vars, asset_state_vars], name='HH') -dag = sj.create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') -# Calibrate steady state -calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, - 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143, 'rho_B': 0.8} +'''Tests''' -ss = dag.solve_steady_state(calibration, dissolve=['fiscal_solved'], solver='hybr', - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -# ss0 = dag.solve_steady_state(calibration, solver='hybr', -# unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, -# targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -# assert all(np.allclose(ss0[k], ss[k]) for k in ss0) +def test_all(): + hh = combine([household, income_state_vars, asset_state_vars], name='HH') + calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, + 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143, 'rho_B': 0.8} + + # DAG with SimpleBlock `fiscal` + dag0 = create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') + ss0 = dag0.solve_steady_state(calibration, solver='hybr', + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -# Precompute household Jacobian -Js = {'household': household.jacobian(ss, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} + # DAG with SolvedBlock `fiscal_solved` + dag1 = create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') + ss1 = dag1.solve_steady_state(calibration, solver='hybr', dissolve=['fiscal_solved'], + unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, + targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) -# Solve Jacobian -G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], - unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) -shock = ImpulseDict({'r': 1E-4*0.9**np.arange(300)}) -td_lin1 = G @ shock + assert all(np.allclose(ss0[k], ss1[k]) for k in ss0) -# Compare to solve_impulse_linear -td_lin2 = dag.solve_impulse_linear(ss, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) -assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) + # Precompute household Jacobian + Js = {'household': household.jacobian(ss1, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} -# Solve impulse_nonlinear -td_hh = hh.impulse_nonlinear(ss, td_lin1[['Y']], outputs=['C', 'A'], Js=Js) + # Linear impulse responses from Jacobian vs directly + G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], + unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) + shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) + td_lin1 = G @ shock + td_lin2 = dag1.solve_impulse_linear(ss1, unknowns=['Y'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) + assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) -td_nonlin = dag.solve_impulse_nonlinear(ss, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) + # Nonlinear vs linear impulses + td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) + assert all(np.allclose(td_lin1[k], td_nonlin[k]) for k in td_lin1) From 5535a7b97e969b31ce53ab7b796f629ad5ce3954 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 24 Aug 2021 18:53:40 -0400 Subject: [PATCH 232/288] more meaningful test test_worflow --- tests/base/test_workflow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index e7abbe8..f80a0e4 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -173,4 +173,5 @@ def test_all(): # Nonlinear vs linear impulses td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) - assert all(np.allclose(td_lin1[k], td_nonlin[k]) for k in td_lin1) + assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 + assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1) From f62b0e9413b6b36ac6224c1257f0c26280d528d0 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 26 Aug 2021 09:59:57 -0500 Subject: [PATCH 233/288] Update _steady_state method of RedirectedBlock to not overwrite entries in the returned `ss` that were populated by the redirect_block --- .../blocks/auxiliary_blocks/redirected_block.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py index 2d1de6a..e57e70e 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py @@ -70,6 +70,13 @@ def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kw """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" if bypass_redirection: kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection - return self.directed._steady_state(calibration, **{k: v for k, v in kwargs.items() if k in self.directed.ss_valid_input_kwargs}) + return self.directed.steady_state(calibration, **{k: v for k, v in kwargs.items() if k in self.directed.ss_valid_input_kwargs}) else: - return super()._steady_state(calibration, dissolve=dissolve, bypass_redirection=bypass_redirection, **kwargs) + ss = calibration.copy() + for block in self.blocks: + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, **kwargs) + # If we're not bypassing redirection, don't overwrite entries in ss populated by the redirect block + ss.update(outputs) if block == self.redirected else ss.update(outputs.difference(ss)) + return ss From 0e8daf98e517713c1aa3ac2f519f1ff1fc1ab9bb Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Thu, 26 Aug 2021 11:02:13 -0500 Subject: [PATCH 234/288] WIP commit to consider automated DAG redirection code --- .../auxiliary_blocks/redirected_block.py | 52 ++++++------- src/sequence_jacobian/steady_state/drivers.py | 3 + src/sequence_jacobian/utilities/graph.py | 74 +++++++++---------- 3 files changed, 67 insertions(+), 62 deletions(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py index e57e70e..da1818d 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py @@ -37,31 +37,33 @@ def __init__(self, block, redirect_block): self.inputs = (redirect_block.inputs | block.inputs) - redirect_block.outputs self.outputs = (redirect_block.outputs | block.outputs) - redirect_block.inputs - # Calculate what are the inputs and outputs of the Block objects underlying `self`, without - # any of the redirecting blocks. - # TODO: Think more carefully about evaluating a DAG with bypass_redirection, if the redirecting - # blocks change the sort order of the DAG!! - if not isinstance(self.directed, Parent): - self.inputs_directed = self.directed.inputs - self.outputs_directed = self.directed.outputs - else: - inputs_directed, outputs_directed = OrderedSet({}), OrderedSet({}) - ps_checked = set({}) - for d in self.directed.descendants: - # The descendant's parent's name (if it has one, o/w the descendant's name) - p = self.directed.descendants[d] - if p is None or p in ps_checked: - continue - else: - ps_checked |= set(p) - if hasattr(self.directed[p], "directed"): - inputs_directed |= self.directed[p].directed.inputs - outputs_directed |= self.directed[p].directed.outputs - else: - inputs_directed |= self[d].inputs - outputs_directed |= self[d].outputs - self.inputs_directed = inputs_directed - outputs_directed - self.outputs_directed = outputs_directed + # TODO: This all may not be necessary so long as there is a function that undoes the + # insertion of all of the redirect blocks and re-sorts the DAG (if necessary) + # # Calculate what are the inputs and outputs of the Block objects underlying `self`, without + # # any of the redirecting blocks. + # # TODO: Think more carefully about evaluating a DAG with bypass_redirection, if the redirecting + # # blocks change the sort order of the DAG!! + # if not isinstance(self.directed, Parent): + # self.inputs_directed = self.directed.inputs + # self.outputs_directed = self.directed.outputs + # else: + # inputs_directed, outputs_directed = OrderedSet({}), OrderedSet({}) + # ps_checked = set({}) + # for d in self.directed.descendants: + # # The descendant's parent's name (if it has one, o/w the descendant's name) + # p = self.directed.descendants[d] + # if p is None or p in ps_checked: + # continue + # else: + # ps_checked |= set(p) + # if hasattr(self.directed[p], "directed"): + # inputs_directed |= self.directed[p].directed.inputs + # outputs_directed |= self.directed[p].directed.outputs + # else: + # inputs_directed |= self[d].inputs + # outputs_directed |= self[d].outputs + # self.inputs_directed = inputs_directed - outputs_directed + # self.outputs_directed = outputs_directed def __repr__(self): return f"" diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index bbee51c..a6a9111 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -101,6 +101,9 @@ def residual(unknown_values, bypass_redirection=False): # Check that the solution is consistent with what would come out of the DAG without the helper blocks if consistency_check: + # TODO: Shouldn't the DAG be re-sorted when the helper blocks are stripped out, so it evaluates through in the + # correct order? The sorted DAG here is based on including the helper blocks! So simply omitting them but + # retaining the order that they created may not be entirely correct? # Add the unknowns not handled by helpers into the DAG to be checked. unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index a22de24..456f033 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -1,34 +1,34 @@ """Topological sort and related code""" -def block_sort(blocks, helper_blocks=None, calibration=None, return_io=False): +def block_sort(blocks, redirect_blocks=None, calibration=None, return_io=False): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's inferred) that indicate their aggregate inputs and outputs - Importantly, because including helper blocks in a blocks without additional measures + Importantly, because including redirect blocks in a blocks without additional measures can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the steady_state computation to resolve these cycles. e.g. Consider Krusell Smith: - Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the helper block - because the "firm" block outputs "r", which the helper block takes as an input. - However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes + Suppose one specifies a redirect block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the redirect block + because the "firm" block outputs "r", which the redirect block takes as an input. + However, it would also include the redirect block as a dependency of the "firm" block because the "firm" block takes "K" as an input. This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of helper block and the cycle would be resolved. + "firm" could be removed as a dependency of redirect block and the cycle would be resolved. blocks: `list` A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - ignore_helpers: `bool` - A boolean indicating whether to account for/return the indices of helper blocks contained in blocks + ignore_redirects: `bool` + A boolean indicating whether to account for/return the indices of redirect blocks contained in blocks Set to true when sorting for td and jac calculations - helper_indices: `list` - A list of indices corresponding to the helper blocks in the blocks + redirect_indices: `list` + A list of indices corresponding to the redirect blocks in the blocks calibration: `dict` or `None` An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using helper blocks. Read above docstring for more detail + introduced by using redirect blocks. Read above docstring for more detail return_io: `bool` A boolean indicating whether to return the full set of input and output arguments from `blocks` """ @@ -37,20 +37,20 @@ def block_sort(blocks, helper_blocks=None, calibration=None, return_io=False): # does clutter up the function body if return_io: # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(blocks, helper_blocks=helper_blocks, + outmap, outargs = construct_output_map(blocks, redirect_blocks=redirect_blocks, return_output_args=True) # step 2: dependency graph for topological sort and input list dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, - helper_blocks=helper_blocks, calibration=calibration) + redirect_blocks=redirect_blocks, calibration=calibration) return topological_sort(dep), inargs, outargs else: # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(blocks, helper_blocks=helper_blocks) + outmap = construct_output_map(blocks, redirect_blocks=redirect_blocks) # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(blocks, outmap, calibration=calibration, helper_blocks=helper_blocks) + dep = construct_dependency_graph(blocks, outmap, calibration=calibration, redirect_blocks=redirect_blocks) return topological_sort(dep) @@ -82,23 +82,23 @@ def topological_sort(dep, names=None): return topsorted -def construct_output_map(blocks, helper_blocks=None, return_output_args=False): +def construct_output_map(blocks, redirect_blocks=None, return_output_args=False): """Construct a map of outputs to the indices of the blocks that produce them. blocks: `list` A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - helper_blocks: `list` - A list of helper blocks, designed to aid steady state computation, to include in the sort + redirect_blocks: `list` + A list of redirect blocks, designed to aid steady state computation, to include in the sort return_output_args: `bool` A boolean indicating whether to track and return the full set of output arguments of all of the blocks in `blocks` """ - if helper_blocks is None: - helper_blocks = [] + if redirect_blocks is None: + redirect_blocks = [] outmap = dict() outargs = set() - for num, block in enumerate(blocks + helper_blocks): + for num, block in enumerate(blocks + redirect_blocks): # Find the relevant set of outputs corresponding to a block if hasattr(block, "outputs"): outputs = block.outputs @@ -108,14 +108,14 @@ def construct_output_map(blocks, helper_blocks=None, return_output_args=False): raise ValueError(f'{block} is not recognized as block or does not provide outputs') for o in outputs: - # Because some of the outputs of a helper block are, by construction, outputs that also appear in the + # Because some of the outputs of a redirect block are, by construction, outputs that also appear in the # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering # throwing this ValueError - if o in outmap and block not in helper_blocks: + if o in outmap and block not in redirect_blocks: raise ValueError(f'{o} is output twice') # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share + # Ensure that the block "outmap" maps "o" to is the actual block and not a redirect block if both share # a given output, such that the dependency graph is constructed on the standard blocks, where possible if o not in outmap: outmap[o] = num @@ -129,7 +129,7 @@ def construct_output_map(blocks, helper_blocks=None, return_output_args=False): return outmap -def construct_dependency_graph(blocks, outmap, helper_blocks=None, +def construct_dependency_graph(blocks, outmap, redirect_blocks=None, calibration=None, return_input_args=False): """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where this set is the set of blocks that the key block is dependent on. @@ -140,12 +140,12 @@ def construct_dependency_graph(blocks, outmap, helper_blocks=None, """ if calibration is None: calibration = {} - if helper_blocks is None: - helper_blocks = [] + if redirect_blocks is None: + redirect_blocks = [] - dep = {num: set() for num in range(len(blocks + helper_blocks))} + dep = {num: set() for num in range(len(blocks + redirect_blocks))} inargs = set() - for num, block in enumerate(blocks + helper_blocks): + for num, block in enumerate(blocks + redirect_blocks): if hasattr(block, 'inputs'): inputs = block.inputs else: @@ -156,12 +156,12 @@ def construct_dependency_graph(blocks, outmap, helper_blocks=None, # Each potential input to a given block will either be 1) output by another block, # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into # the steady-state computation via the `calibration' dict. - # If the block is a helper block, then we want to check the calibration to see if the potential + # If the block is a redirect block, then we want to check the calibration to see if the potential # input is a pre-specified variable/parameter, and if it is then we will not add the block that # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic + # e.g. Krusell Smith's firm_steady_state_solution redirect block and firm block would create a cyclic # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in helper_blocks): + if i in outmap and not (i in calibration and block in redirect_blocks): dep[num].add(outmap[i]) if return_input_args: return dep, inargs @@ -169,17 +169,17 @@ def construct_dependency_graph(blocks, outmap, helper_blocks=None, return dep -def find_outputs_that_are_intermediate_inputs(blocks, helper_blocks=None): +def find_outputs_that_are_intermediate_inputs(blocks, redirect_blocks=None): """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. See the docstring of construct_output_map for more details about the arguments. """ - if helper_blocks is None: - helper_blocks = [] + if redirect_blocks is None: + redirect_blocks = [] required = set() - outmap = construct_output_map(blocks, helper_blocks=helper_blocks) + outmap = construct_output_map(blocks, redirect_blocks=redirect_blocks) for num, block in enumerate(blocks): if hasattr(block, 'inputs'): inputs = block.inputs From 9489396470be194c58959284cf954eeac9e0c595 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 26 Aug 2021 15:32:26 -0500 Subject: [PATCH 235/288] refactored to use DifferentiableExtendedFunction for core backward step function in het_block --- src/sequence_jacobian/blocks/het_block.py | 77 ++++++++------------- src/sequence_jacobian/utilities/function.py | 55 +++++++++++++++ tests/base/test_jacobian.py | 63 ----------------- tests/base/test_workflow.py | 2 + 4 files changed, 87 insertions(+), 110 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 66e3939..5c0ffaa 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -8,6 +8,7 @@ from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict from .support.bijection import Bijection +from ..utilities.function import DifferentiableExtendedFunction, ExtendedFunction def het(exogenous, policy, backward, backward_init=None): @@ -58,14 +59,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) - self.back_step_fun = back_step_fun - - # self.back_step_outputs and self.back_step_inputs are all of the output and input arguments of - # self.back_step_fun, the variables used in the backward iteration, - # which generally include value and/or policy functions. - self.back_step_output_list = utils.misc.output_list(back_step_fun) - self.back_step_outputs = set(self.back_step_output_list) - self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) + self.back_step_fun = ExtendedFunction(back_step_fun) # See the docstring of HetBlock for details on the attributes directly below self.exogenous = exogenous @@ -80,7 +74,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward # iteration variables themselves. - self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) + self.non_back_iter_outputs = self.back_step_fun.outputs - set(self.back_iter_vars) # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used # in utils.graph.block_sort to topologically sort blocks along the DAG @@ -88,7 +82,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # TODO: go back from capitalize to upper!!! (ask Michael first) self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) - self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} + self.inputs = self.back_step_fun.inputs - {k + '_p' for k in self.back_iter_vars} self.inputs.remove(exogenous + '_p') self.inputs.add(exogenous) @@ -107,26 +101,26 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # The set of variables that will be wrapped in a separate namespace for this HetBlock # as opposed to being available at the top level - self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} + self.internal = utils.misc.smart_set(self.back_step_fun.outputs) | utils.misc.smart_set(self.exogenous) | {"D"} if len(self.policy) > 2: raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") # Checking that the various inputs/outputs attributes are correctly set - if self.exogenous + '_p' not in self.back_step_inputs: + if self.exogenous + '_p' not in self.back_step_fun.inputs: raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") for pol in self.policy: - if pol not in self.back_step_outputs: + if pol not in self.back_step_fun.outputs: raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") if pol[0].isupper(): raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") for back in self.back_iter_vars: - if back + '_p' not in self.back_step_inputs: + if back + '_p' not in self.back_step_fun.inputs: raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") - if back not in self.back_step_outputs: + if back not in self.back_step_fun.outputs: raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") for out in self.non_back_iter_outputs: @@ -277,8 +271,7 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) - individual = {k: v for k, v in zip(self.back_step_output_list, - self.back_step_fun(**self.make_inputs(backdict)))} + individual = self.back_step_fun(self.make_inputs(backdict)) backdict.update({k: individual[k] for k in self.back_iter_vars}) if self.hetoutput is not None: @@ -344,16 +337,16 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: this is one instance of us letting people supply inputs that aren't actually inputs # This behavior should lead to an error instead (probably should be handled at top level) - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in inputs] + relevant_shocks = [i for i in self.back_step_fun.inputs | self.hetinput_inputs if i in inputs] # step 0: preliminary processing of steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = self.jac_prelim(ss) + Pi, differentiable_back_step_fun, ss_for_hetinput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in relevant_shocks: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, + curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, ss.internal[self.name]['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, h, ss_for_hetinput) @@ -442,7 +435,7 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation # but is computed after the backward iteration - self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) + self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_fun.outputs - self.hetoutput_outputs - set("D")) # Modify the HetBlock's outputs to include the aggregated hetoutputs self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) self.M_outputs = Bijection({o: o.capitalize() for o in self.hetoutput_outputs}) @ self.M_outputs @@ -480,7 +473,7 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): for it in range(maxit): try: # run and store results of backward iteration, which come as tuple, in dict - sspol = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**ssin))} + sspol = self.back_step_fun(ssin) except KeyError as e: print(f'Missing input {e} to {self.back_step_fun.__name__}!') raise @@ -571,25 +564,15 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see - Step 4: J_from_F to get Jacobian from fake news matrix ''' - def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=1E-4): + def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space): # shock perturbs outputs - shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, - utils.differentiate.numerical_diff(self.back_step_fun, - ssin_dict, din_dict, h, - ssout_list))} + shocked_outputs = differentiable_back_step_fun.diff(din_dict) curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} # which affects the distribution tomorrow pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} - # Include an additional term to account for the effect of a deleveraging shock affecting the grid - if "delev_exante" in din_dict: - dx = np.zeros_like(sspol_pi["a"]) - dx[sspol_i["a"] == 0] = 1. - add_term = sspol_pi["a"] * dx / sspol_space["a"] - pol_pi_shock["a"] += add_term - curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) # and the aggregate outcomes today @@ -597,8 +580,8 @@ def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, return curlyV, curlyD, curlyY - def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, Pi_T, - sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): + def backward_iteration_fakenews(self, input_shocked, output_list, differentiable_back_step_fun, Dss, Pi_T, + sspol_i, sspol_pi, sspol_space, T, h, ss_for_hetinput=None): """Iterate policy steps backward T times for a single shock.""" # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None # since unless self.hetinput_inputs is exactly equal to input_shocked, calling @@ -614,8 +597,8 @@ def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, sso din_dict = {input_shocked: 1} # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h=h) + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space) # infer dimensions from this and initialize empty arrays curlyDs = np.empty((T,) + curlyD.shape) @@ -629,8 +612,8 @@ def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, sso # fill in anticipation effects for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, ssin_dict, ssout_list, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space, h) + output_list, differentiable_back_step_fun, + Dss, Pi_T, sspol_i, sspol_pi, sspol_space) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] @@ -669,7 +652,7 @@ def J_from_F(F): '''Part 5: helpers for .jac and .ajac: preliminary processing''' - def jac_prelim(self, ss): + def jac_prelim(self, ss, h): """Helper that does preliminary processing of steady state for fake news algorithm. Parameters @@ -691,7 +674,9 @@ def jac_prelim(self, ss): ssin_dict = self.make_inputs(ss) Pi = ss.internal[self.name][self.exogenous] grid = {k: ss[k+'_grid'] for k in self.policy} - ssout_list = self.back_step_fun(**ssin_dict) + #ssout_list = self.back_step_fun(ssin_dict) + differentiable_back_step_fun = self.back_step_fun.differentiable(ssin_dict, h=h) + ss_for_hetinput = None if self.hetinput is not None: @@ -706,9 +691,7 @@ def jac_prelim(self, ss): sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - toreturn = (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) - - return toreturn + return Pi, differentiable_back_step_fun, ss_for_hetinput, sspol_i, sspol_pi, sspol_space '''Part 6: helper to extract inputs and potentially process them through hetinput''' @@ -748,7 +731,7 @@ def make_inputs(self, back_step_inputs_dict): del input_dict[i_p] try: - return {k: input_dict[k] for k in self.back_step_inputs if k in input_dict} + return {k: input_dict[k] for k in self.back_step_fun.inputs if k in input_dict} except KeyError as e: print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') raise diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 9df1696..7befe4e 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -1,6 +1,7 @@ from sequence_jacobian.utilities.ordered_set import OrderedSet import re import inspect +import numpy as np # TODO: fix this, have it twice (main version in misc) due to circular import problem # let's make everything point to here for input_list, etc. so that this is unnecessary @@ -82,3 +83,57 @@ def wrapped_call(self, input_dict, preprocess=None, postprocess=None): output_dict = {k: postprocess(v) for k, v in output_dict.items()} return output_dict + + def differentiable(self, input_dict, h=1E-6, h2=1E-4): + return DifferentiableExtendedFunction(self.f, self.name, self.inputs, self.outputs, input_dict, h, h2) + + +class DifferentiableExtendedFunction(ExtendedFunction): + def __init__(self, f, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4): + self.f, self.name, self.inputs, self.outputs = f, name, inputs, outputs + self.input_dict = input_dict + self.output_dict = None # lazy evaluation of outputs for one-sided diff + self.h = h + self.h2 = h2 + + def diff(self, shock_dict, h=None, hide_zeros=False): + if h is None: + h = self.h + + if self.output_dict is None: + self.output_dict = self(self.input_dict) + + shocked_input_dict = {**self.input_dict, + **{k: self.input_dict[k] + h * shock for k, shock in shock_dict.items() if k in self.input_dict}} + + shocked_output_dict = self(shocked_input_dict) + + derivative_dict = {k: (shocked_output_dict[k] - self.output_dict[k])/h for k in self.output_dict} + + if hide_zeros: + derivative_dict = hide_zero_values(derivative_dict) + + return derivative_dict + + def diff2(self, shock_dict, h=None, hide_zeros=False): + if h is None: + h = self.h2 + + shocked_input_dict_up = {**self.input_dict, + **{k: self.input_dict[k] + h * shock for k, shock in shock_dict.items() if k in self.input_dict}} + shocked_input_dict_dn = {**self.input_dict, + **{k: self.input_dict[k] - h * shock for k, shock in shock_dict.items() if k in self.input_dict}} + + shocked_output_dict_up = self(shocked_input_dict_up) + shocked_output_dict_dn = self(shocked_input_dict_dn) + + derivative_dict = {k: (shocked_output_dict_up[k] - shocked_output_dict_dn[k])/(2*h) for k in shocked_output_dict_dn} + + if hide_zeros: + derivative_dict = hide_zero_values(derivative_dict) + + return derivative_dict + + +def hide_zero_values(d): + return {k: v for k, v in d.items() if not np.allclose(v, 0)} diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 95813cc..28997bb 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -57,69 +57,6 @@ def test_ks_jac(krusell_smith_dag): # assert np.allclose(G[o][i], G2[o][i]) -def test_fake_news_v_actual(one_asset_hank_dag): - hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag - - household = hank_model._blocks_unsorted[0] - T = 40 - exogenous = ['w', 'r', 'Div', 'Tax'] - Js = household.jacobian(ss, exogenous, T=T) - output_list = household.non_back_iter_outputs - - # Preliminary processing of the steady state - (ssin_dict, Pi, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space) = household.jac_prelim(ss) - - # Step 1 of fake news algorithm: backward iteration - h = 1E-4 - curlyYs, curlyDs = {}, {} - for i in exogenous: - curlyYs[i], curlyDs[i] = household.backward_iteration_fakenews(i, output_list, ssin_dict, - ssout_list, ss.internal["household"]['D'], - Pi.T.copy(), sspol_i, sspol_pi, sspol_space, - T, h, ss_for_hetinput) - - asset_effects = np.sum(curlyDs['r'] * ss['a_grid'], axis=(1, 2)) - assert np.linalg.norm(asset_effects - curlyYs["r"]["a"], np.inf) < 2e-15 - - # Step 2 of fake news algorithm: (transpose) forward iteration - curlyPs = {} - for o in output_list: - curlyPs[o] = household.forward_iteration_fakenews(ss.internal["household"][o], Pi, sspol_i, sspol_pi, T-1) - - persistent_asset = np.array([np.vdot(curlyDs['r'][0, ...], - curlyPs['a'][u, ...]) for u in range(30)]) - - assert np.linalg.norm(persistent_asset - Js["A"]["r"][1:31, 0], np.inf) < 3e-15 - - # Step 3 of fake news algorithm: combine everything to make the fake news matrix for each output-input pair - Fs = {o.capitalize(): {} for o in output_list} - for o in output_list: - for i in exogenous: - F = np.empty((T,T)) - F[0, ...] = curlyYs[i][o] - F[1:, ...] = curlyPs[o].reshape(T-1, -1) @ curlyDs[i].reshape(T, -1).T - Fs[o.capitalize()][i] = F - - impulse = Fs['C']['w'][:10, 1].copy() # start with fake news impulse - impulse[1:10] += Js['C']['w'][:9, 0] # add unanticipated impulse, shifted by 1 - - assert np.linalg.norm(impulse - Js["C"]["w"][:10, 1], np.inf) == 0.0 - - # Step 4 of fake news algorithm: recursively convert fake news matrices to actual Jacobian matrices - Js_original = Js - Js = {o.capitalize(): {} for o in output_list} - for o in output_list: - for i in exogenous: - # implement recursion (30): start with J=F and accumulate terms along diagonal - J = Fs[o.capitalize()][i].copy() - for t in range(1, J.shape[1]): - J[1:, t] += J[:-1, t-1] - Js[o.capitalize()][i] = J - - for o in output_list: - for i in exogenous: - assert np.array_equal(Js[o.capitalize()][i], Js_original[o.capitalize()][i]) - def test_fake_news_v_direct_method(one_asset_hank_dag): hank_model, _, _, _, ss = one_asset_hank_dag diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index f80a0e4..13d9162 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -175,3 +175,5 @@ def test_all(): inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1) + +test_all() From 0b7cb70c4f13e0130fdffc60e6c5be578c5a7819 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 26 Aug 2021 16:30:52 -0500 Subject: [PATCH 236/288] refactored to use ExtendedFunction and DifferentiableExtendedFunction for everything in het_block --- src/sequence_jacobian/__init__.py | 2 +- src/sequence_jacobian/blocks/het_block.py | 208 ++++++---------------- src/sequence_jacobian/models/two_asset.py | 3 +- tests/base/test_workflow.py | 5 +- 4 files changed, 60 insertions(+), 158 deletions(-) diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index 72f8d84..525c6cc 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -3,7 +3,7 @@ from . import estimation, jacobian, utilities, devtools from .blocks.simple_block import simple -from .blocks.het_block import het, hetoutput +from .blocks.het_block import het from .blocks.solved_block import solved from .blocks.combined_block import combine, create_model from .blocks.support.simple_displacement import apply_function diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 5c0ffaa..432b045 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -89,15 +89,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. self.hetinput = None - self.hetinput_inputs = set() - self.hetinput_outputs = set() - self.hetinput_outputs_order = tuple() - - # start without a hetoutput self.hetoutput = None - self.hetoutput_inputs = set() - self.hetoutput_outputs = set() - self.hetoutput_outputs_order = tuple() # The set of variables that will be wrapped in a separate namespace for this HetBlock # as opposed to being available at the top level @@ -127,13 +119,9 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non if out[0].isupper(): raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") - # Add the backward iteration initializer function (the initial guesses for self.back_iter_vars) - if backward_init is None: - # TODO: Think about implementing some "automated way" of providing - # an initial guess for the backward iteration. - self.backward_init = backward_init - else: - self.backward_init = backward_init + if backward_init is not None: + backward_init = ExtendedFunction(backward_init) + self.backward_init = backward_init # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. @@ -141,10 +129,10 @@ def __repr__(self): """Nice string representation of HetBlock for printing to console""" if self.hetinput is not None: if self.hetoutput is not None: - return f"" else: - return f"" + return f"" else: return f"" @@ -197,7 +185,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars """ - ss = copy.deepcopy(calibration.toplevel) + ss = calibration.toplevel.copy() # extract information from calibration Pi = ss[self.exogenous] @@ -213,17 +201,16 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) ss.update({"D": D}) - # aggregate all outputs other than backward variables on grid, capitalize - aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} - ss.update(aggregates) + # run hetoutput if it's there + if self.hetoutput is not None: + ss.update(self.hetoutput(ss)) + # aggregate all outputs other than backward variables on grid, capitalize + toreturn = self.non_back_iter_outputs if self.hetoutput is not None: - hetoutputs = self.hetoutput.evaluate(ss) - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") - else: - hetoutputs = {} - aggregate_hetoutputs = {} - ss.update({**hetoutputs, **aggregate_hetoutputs}) + toreturn = toreturn | self.hetoutput.outputs + aggregates = {o.capitalize(): np.vdot(D, ss[o]) for o in toreturn} + ss.update(aggregates) return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, {self.name: {k: ss[k] for k in ss if k in self.internal}}) @@ -262,8 +249,10 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind use_ss_grid[k] = True # allocate empty arrays to store result, assume all like D - individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} - hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} + toreturn = self.non_back_iter_outputs + if self.hetoutput is not None: + toreturn = toreturn | self.hetoutput.outputs + individual_paths = {k: np.empty((T,) + D.shape) for k in toreturn} # backward iteration backdict = dict(ss.items()) @@ -271,13 +260,12 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) - individual = self.back_step_fun(self.make_inputs(backdict)) + individual = self.make_inputs(backdict) + individual.update(self.back_step_fun(individual)) backdict.update({k: individual[k] for k in self.back_iter_vars}) if self.hetoutput is not None: - hetoutput = self.hetoutput.evaluate(backdict) - for k in self.hetoutput_outputs: - hetoutput_paths[k][t, ...] = hetoutput[k] + individual.update(self.hetoutput(individual)) for k in self.non_back_iter_outputs: individual_paths[k][t, ...] = individual[k] @@ -306,19 +294,14 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind # obtain aggregates of all outputs, made uppercase aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) - for o in self.non_back_iter_outputs} - if self.hetoutput: - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutput_paths, D_path, backdict, mode="td") - else: - aggregate_hetoutputs = {} + for o in individual_paths} # return either this, or also include distributional information # TODO: rethink this if returnindividual: - return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **hetoutput_paths, - 'D': D_path}) - ss + return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ss else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs})[outputs] - ss + return ImpulseDict(aggregates)[outputs] - ss def _impulse_linear(self, ss, inputs, outputs, Js): @@ -337,10 +320,13 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: this is one instance of us letting people supply inputs that aren't actually inputs # This behavior should lead to an error instead (probably should be handled at top level) - relevant_shocks = [i for i in self.back_step_fun.inputs | self.hetinput_inputs if i in inputs] + relevant_shocks = self.back_step_fun.inputs + if self.hetinput is not None: + relevant_shocks = relevant_shocks | self.hetinput.inputs + relevant_shocks = relevant_shocks & inputs # step 0: preliminary processing of steady state - Pi, differentiable_back_step_fun, ss_for_hetinput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) + Pi, differentiable_back_step_fun, differentiable_hetinput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i @@ -348,8 +334,8 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): for i in relevant_shocks: curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, ss.internal[self.name]['D'], Pi.T.copy(), - sspol_i, sspol_pi, sspol_space, T, h, - ss_for_hetinput) + sspol_i, sspol_pi, sspol_space, T, + differentiable_hetinput) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o @@ -392,16 +378,11 @@ def add_hetinput(self, hetinput, overwrite=False, verbose=True): else: print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") - self.hetinput = hetinput - self.hetinput_inputs = set(utils.misc.input_list(hetinput)) - self.hetinput_outputs = set(utils.misc.output_list(hetinput)) - self.hetinput_outputs_order = utils.misc.output_list(hetinput) - - # modify inputs to include hetinput's additional inputs, remove outputs - self.inputs |= self.hetinput_inputs - self.inputs -= self.hetinput_outputs + self.hetinput = ExtendedFunction(hetinput) + self.inputs |= self.hetinput.inputs + self.inputs -= self.hetinput.outputs - self.internal |= self.hetinput_outputs + self.internal |= self.hetinput.outputs def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process @@ -424,10 +405,7 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): else: print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") - self.hetoutput = hetoutput - self.hetoutput_inputs = set(hetoutput.input_list) - self.hetoutput_outputs = set(hetoutput.output_list) - self.hetoutput_outputs_order = hetoutput.output_list + self.hetoutput = ExtendedFunction(hetoutput) # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput # and aggregating the hetoutput, but do not include: @@ -435,12 +413,12 @@ def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation # but is computed after the backward iteration - self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_fun.outputs - self.hetoutput_outputs - set("D")) + self.inputs |= (self.hetoutput.inputs - self.hetinput.outputs - self.back_step_fun.outputs - self.hetoutput.outputs - set("D")) # Modify the HetBlock's outputs to include the aggregated hetoutputs - self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) - self.M_outputs = Bijection({o: o.capitalize() for o in self.hetoutput_outputs}) @ self.M_outputs + self.outputs |= set([o.capitalize() for o in self.hetoutput.outputs]) + self.M_outputs = Bijection({o: o.capitalize() for o in self.hetoutput.outputs}) @ self.M_outputs - self.internal |= self.hetoutput_outputs + self.internal |= self.hetoutput.outputs '''Part 3: components of ss(): - policy_ss : backward iteration to get steady-state policies and other outcomes @@ -494,7 +472,7 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): ssin[k] = ssin[k + '_p'] del ssin[k + '_p'] if self.hetinput is not None: - for k in self.hetinput_inputs: + for k in self.hetinput.inputs: if k in original_ssin: ssin[k] = original_ssin[k] return {**ssin, **sspol} @@ -581,17 +559,15 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step return curlyV, curlyD, curlyY def backward_iteration_fakenews(self, input_shocked, output_list, differentiable_back_step_fun, Dss, Pi_T, - sspol_i, sspol_pi, sspol_space, T, h, ss_for_hetinput=None): + sspol_i, sspol_pi, sspol_space, T, differentiable_hetinput): """Iterate policy steps backward T times for a single shock.""" # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None # since unless self.hetinput_inputs is exactly equal to input_shocked, calling # self.hetinput() inside the symmetric differentiation function will throw an error. # It's probably better/more informative to throw that error out here. - if self.hetinput is not None and input_shocked in self.hetinput_inputs: + if self.hetinput is not None and input_shocked in self.hetinput.inputs: # if input_shocked is an input to hetinput, take numerical diff to get response - din_dict = dict(zip(self.hetinput_outputs_order, - utils.differentiate.numerical_diff_symmetric(self.hetinput, - ss_for_hetinput, {input_shocked: 1}, h))) + din_dict = differentiable_hetinput.diff2({input_shocked: 1}) else: # otherwise, we just have that one shock din_dict = {input_shocked: 1} @@ -677,10 +653,10 @@ def jac_prelim(self, ss, h): #ssout_list = self.back_step_fun(ssin_dict) differentiable_back_step_fun = self.back_step_fun.differentiable(ssin_dict, h=h) - - ss_for_hetinput = None + differentiable_hetinput = None if self.hetinput is not None: - ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} + # ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} + differentiable_hetinput = self.hetinput.differentiable(ss) # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints sspol_i = {} @@ -691,7 +667,7 @@ def jac_prelim(self, ss, h): sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - return Pi, differentiable_back_step_fun, ss_for_hetinput, sspol_i, sspol_pi, sspol_space + return Pi, differentiable_back_step_fun, differentiable_hetinput, sspol_i, sspol_pi, sspol_space '''Part 6: helper to extract inputs and potentially process them through hetinput''' @@ -699,32 +675,16 @@ def make_inputs(self, back_step_inputs_dict): """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, process stuff through self.hetinput first if it's there. """ - input_dict = copy.deepcopy(back_step_inputs_dict) - - # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady - # state computation as well as for Jacobian/impulse evaluation for HetBlocks, - # where in the former the hetinputs and value function have yet to be computed, - # whereas in the latter they have already been computed - # and hence do not need to be recomputed. There may be room to clean this function up a bit. if isinstance(back_step_inputs_dict, SteadyStateDict): - input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) - input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) + input_dict = {**back_step_inputs_dict.toplevel, **back_step_inputs_dict.internal[self.name]} else: - # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include - # them as inputs for self.back_step_fun - if self.hetinput is not None: - outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] - for k in self.hetinput_inputs if k in input_dict})) - input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) - - # Check if there are entries in indict corresponding to self.inputs_to_be_primed. - # In particular, we are interested in knowing if an initial value - # for the backward iteration variable has been provided. - # If it has not been provided, then use self.backward_init to calculate the initial values. - if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): - initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] - input_dict.update(zip(utils.misc.output_list(self.backward_init), - utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) + input_dict = back_step_inputs_dict.copy() + + if self.hetinput is not None: + input_dict.update(self.hetinput(input_dict)) + + if not all(k in input_dict for k in self.back_iter_vars): + input_dict.update(self.backward_init(input_dict)) for i_p in self.inputs_to_be_primed: input_dict[i_p + "_p"] = input_dict[i_p] @@ -785,59 +745,3 @@ def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): else: raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - -def hetoutput(custom_aggregation=None): - def decorator(f): - return HetOutput(f, custom_aggregation=custom_aggregation) - return decorator - - -class HetOutput: - def __init__(self, f, custom_aggregation=None): - self.name = f.__name__ - self.f = f - self.eval_input_list = utils.misc.input_list(f) - - self.custom_aggregation = custom_aggregation - self.agg_input_list = [] if custom_aggregation is None else utils.misc.input_list(custom_aggregation) - - # We are distinguishing between the eval_input_list and agg_input_list because custom aggregation may require - # certain arguments that are not required for simply evaluating the hetoutput - self.input_list = list(set(self.eval_input_list).union(set(self.agg_input_list))) - self.output_list = utils.misc.output_list(f) - - def evaluate(self, arg_dict): - hetoutputs = dict(zip(self.output_list, utils.misc.make_tuple(self.f(*[arg_dict[i] for i - in self.eval_input_list])))) - return hetoutputs - - def aggregate(self, hetoutputs, D, custom_aggregation_args, mode="ss"): - if self.custom_aggregation is not None: - hetoutputs_w_std_aggregation = list(set(self.output_list) - - set([utils.misc.uncapitalize(o) for o - in utils.misc.output_list(self.custom_aggregation)])) - hetoutputs_w_custom_aggregation = list(set(self.output_list) - set(hetoutputs_w_std_aggregation)) - else: - hetoutputs_w_std_aggregation = self.output_list - hetoutputs_w_custom_aggregation = [] - - # TODO: May need to check if this works properly for td - if self.custom_aggregation is not None: - hetoutputs_w_custom_aggregation_args = dict(zip(hetoutputs_w_custom_aggregation, - [hetoutputs[i] for i in hetoutputs_w_custom_aggregation])) - custom_agg_inputs = {"D": D, **hetoutputs_w_custom_aggregation_args, **custom_aggregation_args} - custom_aggregates = dict(zip([o.capitalize() for o in hetoutputs_w_custom_aggregation], - utils.misc.make_tuple(self.custom_aggregation(*[custom_agg_inputs[i] for i - in self.agg_input_list])))) - else: - custom_aggregates = {} - - if mode == "ss": - std_aggregates = {o.capitalize(): np.vdot(D, hetoutputs[o]) for o in hetoutputs_w_std_aggregation} - elif mode == "td": - std_aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D, hetoutputs[o]) - for o in hetoutputs_w_std_aggregation} - else: - raise RuntimeError(f"Mode {mode} is not supported in HetOutput aggregation. Choose either 'ss' or 'td'") - - return {**std_aggregates, **custom_aggregates} diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 241cb45..d9e7fc2 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -4,7 +4,7 @@ from .. import utilities as utils from ..blocks.simple_block import simple -from ..blocks.het_block import het, hetoutput +from ..blocks.het_block import het from ..blocks.solved_block import solved from ..blocks.support.simple_displacement import apply_function from ..blocks.combined_block import combine @@ -116,7 +116,6 @@ def income(e_grid, tax, w, N): # A potential hetoutput to include with the above HetBlock -@hetoutput() def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) return chi diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 13d9162..e25403c 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -1,6 +1,6 @@ import numpy as np import sequence_jacobian as sj -from sequence_jacobian import het, simple, hetoutput, solved, combine, create_model +from sequence_jacobian import het, simple, solved, combine, create_model from sequence_jacobian.blocks.support.impulse import ImpulseDict @@ -85,7 +85,6 @@ def asset_state_vars(amin, amax, nA): return a_grid -@hetoutput() def mpcs(c, a, a_grid, rpost): """MPC out of lump-sum transfer.""" mpc = get_mpcs(c, a, a_grid, rpost) @@ -176,4 +175,4 @@ def test_all(): assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1) -test_all() +#test_all() From 99b92ab66a49101c754b6f077016654a87fb53cb Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 26 Aug 2021 16:35:14 -0500 Subject: [PATCH 237/288] removed unnecessary input handling in het_block._jacobian --- src/sequence_jacobian/blocks/het_block.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 432b045..3a552b0 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -318,20 +318,13 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): """ outputs = self.M_outputs.inv @ outputs # horrible - # TODO: this is one instance of us letting people supply inputs that aren't actually inputs - # This behavior should lead to an error instead (probably should be handled at top level) - relevant_shocks = self.back_step_fun.inputs - if self.hetinput is not None: - relevant_shocks = relevant_shocks | self.hetinput.inputs - relevant_shocks = relevant_shocks & inputs - # step 0: preliminary processing of steady state Pi, differentiable_back_step_fun, differentiable_hetinput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} - for i in relevant_shocks: + for i in inputs: curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, ss.internal[self.name]['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, @@ -347,7 +340,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # make fake news matrix and Jacobian for each outcome-input pair F, J = {}, {} for o in outputs: - for i in relevant_shocks: + for i in inputs: if o.capitalize() not in F: F[o.capitalize()] = {} if o.capitalize() not in J: From 14b0c273e68f719b1fee86c47fd3878f8114b85a Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 26 Aug 2021 17:18:33 -0500 Subject: [PATCH 238/288] HetOutput now works with Jacobian so that all tests pass --- src/sequence_jacobian/blocks/het_block.py | 36 +++++++++++++---------- tests/base/test_workflow.py | 9 +++--- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 3a552b0..d7fdc5a 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -267,7 +267,7 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind if self.hetoutput is not None: individual.update(self.hetoutput(individual)) - for k in self.non_back_iter_outputs: + for k in individual_paths: individual_paths[k][t, ...] = individual[k] D_path = np.empty((T,) + D.shape) @@ -316,25 +316,26 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): h : [optional] float h for numerical differentiation of backward iteration """ + ss = {**ss.toplevel, **ss.internal[self.name]} outputs = self.M_outputs.inv @ outputs # horrible # step 0: preliminary processing of steady state - Pi, differentiable_back_step_fun, differentiable_hetinput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) + Pi, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in inputs: curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, - ss.internal[self.name]['D'], Pi.T.copy(), + ss['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, - differentiable_hetinput) + differentiable_hetinput, differentiable_hetoutput) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o curlyPs = {} for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, sspol_i, sspol_pi, T-1) + curlyPs[o] = self.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair @@ -536,7 +537,7 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see ''' def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space): + differentiable_hetoutput, Dss, Pi_T, sspol_i, sspol_pi, sspol_space): # shock perturbs outputs shocked_outputs = differentiable_back_step_fun.diff(din_dict) curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} @@ -547,12 +548,14 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) # and the aggregate outcomes today + if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): + shocked_outputs.update(differentiable_hetoutput.diff(shocked_outputs)) curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} return curlyV, curlyD, curlyY def backward_iteration_fakenews(self, input_shocked, output_list, differentiable_back_step_fun, Dss, Pi_T, - sspol_i, sspol_pi, sspol_space, T, differentiable_hetinput): + sspol_i, sspol_pi, sspol_space, T, differentiable_hetinput, differentiable_hetoutput): """Iterate policy steps backward T times for a single shock.""" # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None # since unless self.hetinput_inputs is exactly equal to input_shocked, calling @@ -566,7 +569,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, differentiable din_dict = {input_shocked: 1} # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, differentiable_hetoutput, Dss, Pi_T, sspol_i, sspol_pi, sspol_space) # infer dimensions from this and initialize empty arrays @@ -581,7 +584,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, differentiable # fill in anticipation effects for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, differentiable_back_step_fun, + output_list, differentiable_back_step_fun, differentiable_hetoutput, Dss, Pi_T, sspol_i, sspol_pi, sspol_space) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] @@ -639,28 +642,29 @@ def jac_prelim(self, ss, h): sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy """ - # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation - ssin_dict = self.make_inputs(ss) - Pi = ss.internal[self.name][self.exogenous] + Pi = ss[self.exogenous] grid = {k: ss[k+'_grid'] for k in self.policy} - #ssout_list = self.back_step_fun(ssin_dict) - differentiable_back_step_fun = self.back_step_fun.differentiable(ssin_dict, h=h) + differentiable_back_step_fun = self.back_step_fun.differentiable(self.make_inputs(ss), h=h) differentiable_hetinput = None if self.hetinput is not None: # ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} differentiable_hetinput = self.hetinput.differentiable(ss) + differentiable_hetoutput = None + if self.hetoutput is not None: + differentiable_hetoutput = self.hetoutput.differentiable(ss) + # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints sspol_i = {} sspol_pi = {} sspol_space = {} for pol in self.policy: # use robust binary-search-based method that only requires grids to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss.internal[self.name][pol]) + sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss[pol]) sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - return Pi, differentiable_back_step_fun, differentiable_hetinput, sspol_i, sspol_pi, sspol_space + return Pi, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, sspol_i, sspol_pi, sspol_space '''Part 6: helper to extract inputs and potentially process them through hetinput''' diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index e25403c..9546eec 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -161,18 +161,17 @@ def test_all(): Js = {'household': household.jacobian(ss1, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} # Linear impulse responses from Jacobian vs directly - G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], + G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) td_lin1 = G @ shock td_lin2 = dag1.solve_impulse_linear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) + inputs=shock, outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], Js=Js) assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) # Nonlinear vs linear impulses td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'asset_mkt', 'goods_mkt'], Js=Js) + inputs=shock, outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1) + assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'Mpc') -#test_all() From 0fa40baed84f9acb36d028463a01d6b82d5f5fd9 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 27 Aug 2021 14:12:48 -0500 Subject: [PATCH 239/288] added extended parallel functions, planned for use in HetInput and HetOutput --- src/sequence_jacobian/utilities/function.py | 78 ++++++++++++++++++++- tests/utils/test_function.py | 42 ++++++++++- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 7befe4e..92d8e82 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -63,8 +63,11 @@ class ExtendedFunction: inputs, returns dict containing outputs by name""" def __init__(self, f): - self.f = f - self.name, self.inputs, self.outputs = metadata(f) + if isinstance(f, ExtendedFunction): + self.f, self.name, self.inputs, self.outputs = f.f, f.name, f.inputs, f.outputs + else: + self.f = f + self.name, self.inputs, self.outputs = metadata(f) def __call__(self, input_dict): # take subdict of d contained in inputs @@ -137,3 +140,74 @@ def diff2(self, shock_dict, h=None, hide_zeros=False): def hide_zero_values(d): return {k: v for k, v in d.items() if not np.allclose(v, 0)} + + +class ExtendedParallelFunction(ExtendedFunction): + def __init__(self, fs, name=None): + inputs = OrderedSet([]) + outputs = OrderedSet([]) + functions = {} + for f in fs: + ext_f = ExtendedFunction(f) + if not outputs.isdisjoint(ext_f.outputs): + raise ValueError(f'Overlap in outputs of ParallelFunction: {ext_f.name} and others both have {outputs & ext_f.outputs}') + inputs |= ext_f.inputs + outputs |= ext_f.outputs + functions[ext_f.name] = ext_f + + self.inputs = inputs + self.outputs = outputs + self.functions = functions + + if name is None: + names = list(functions) + if len(names) == 1: + self.name = names[0] + else: + self.name = f'{names[0]}_{names[-1]}' + else: + self.name = name + + def __call__(self, input_dict): + results = {} + for f in self.functions.values(): + results.update(f(input_dict)) + return results + + def call_on_deviations(self, ss, dev_dict): + results = {} + input_dict = {**ss, **dev_dict} + for f in self.functions.values(): + if not f.inputs.isdisjoint(dev_dict): + results.update(f(input_dict)) + return results + + def wrapped_call(self, input_dict, preprocess=None, postprocess=None): + raise NotImplementedError + + def differentiable(self, input_dict, h=1E-6, h2=1E-4): + return DifferentiableExtendedParallelFunction(self.functions, self.name, self.inputs, self.outputs, input_dict, h, h2) + + +class DifferentiableExtendedParallelFunction(ExtendedParallelFunction, DifferentiableExtendedFunction): + def __init__(self, functions, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4): + self.name, self.inputs, self.outputs = name, inputs, outputs + diff_functions = {} + for k, f in functions.items(): + diff_functions[k] = f.differentiable(input_dict, h, h2) + self.diff_functions = diff_functions + + def diff(self, shock_dict, h=None, hide_zeros=False): + results = {} + for f in self.diff_functions.values(): + if not f.inputs.isdisjoint(shock_dict): + results.update(f.diff(shock_dict, h, hide_zeros)) + return results + + def diff2(self, shock_dict, h=None, hide_zeros=False): + results = {} + for f in self.diff_functions.values(): + if not f.inputs.isdisjoint(shock_dict): + results.update(f.diff2(shock_dict, h, hide_zeros)) + return results + diff --git a/tests/utils/test_function.py b/tests/utils/test_function.py index d3c9a48..8364164 100644 --- a/tests/utils/test_function.py +++ b/tests/utils/test_function.py @@ -1,6 +1,6 @@ from sequence_jacobian.utilities.ordered_set import OrderedSet -from sequence_jacobian.utilities.function import ExtendedFunction, metadata - +from sequence_jacobian.utilities.function import DifferentiableExtendedFunction, ExtendedFunction, ExtendedParallelFunction, metadata +import numpy as np def f1(a, b, c): k = a + 1 @@ -22,3 +22,41 @@ def test_extended_function(): assert ExtendedFunction(f1)(inputs) == {'k': 2, 'l': -1} assert ExtendedFunction(f2)(inputs) == {'k': 6} + +def f3(a, b): + c = a*b - 3*a + d = 3*b**2 + return c, d + + +def test_differentiable_extended_function(): + extf3 = ExtendedFunction(f3) + + ss1 = {'a': 1, 'b': 2} + inputs1 = {'a': 0.5} + + diff = extf3.differentiable(ss1).diff(inputs1) + assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['d'], 0) + + +def f4(a, e): + f = a / e + return f + + +def test_differentiable_extended_parallel_function(): + fs = ExtendedParallelFunction([f3, f4]) + + ss1 = {'a': 1, 'b': 2, 'e': 4} + inputs1 = {'a': 0.5, 'e': 1} + + diff = fs.differentiable(ss1).diff(inputs1) + assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['d'], 0) + assert np.isclose(diff['f'], 1/16) + + inputs2 = {'e': -2} + diff = fs.differentiable(ss1).diff2(inputs2) + assert list(diff) == ['f'] + assert np.isclose(diff['f'], 1/8) From fe5194d7eb69c571584caecb9ed3668621551629 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 27 Aug 2021 14:24:25 -0500 Subject: [PATCH 240/288] added output handling for ExtendedParallelFunctions --- src/sequence_jacobian/utilities/function.py | 30 ++++++++++++++------- tests/utils/test_function.py | 12 +++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 92d8e82..1fb77eb 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -168,18 +168,24 @@ def __init__(self, fs, name=None): else: self.name = name - def __call__(self, input_dict): + def __call__(self, input_dict, outputs=None): results = {} - for f in self.functions.values(): - results.update(f(input_dict)) + for f in self.functions.values(): + if outputs is None or not f.outputs.isdisjoint(outputs): + results.update(f(input_dict)) + if outputs is not None: + results = {k: results[k] for k in outputs} return results - def call_on_deviations(self, ss, dev_dict): + def call_on_deviations(self, ss, dev_dict, outputs=None): results = {} input_dict = {**ss, **dev_dict} for f in self.functions.values(): if not f.inputs.isdisjoint(dev_dict): - results.update(f(input_dict)) + if outputs is None or not f.outputs.isdisjoint(outputs): + results.update(f(input_dict)) + if outputs is not None: + results = {k: results[k] for k in outputs if k in results} return results def wrapped_call(self, input_dict, preprocess=None, postprocess=None): @@ -197,17 +203,23 @@ def __init__(self, functions, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4 diff_functions[k] = f.differentiable(input_dict, h, h2) self.diff_functions = diff_functions - def diff(self, shock_dict, h=None, hide_zeros=False): + def diff(self, shock_dict, h=None, outputs=None, hide_zeros=False): results = {} for f in self.diff_functions.values(): if not f.inputs.isdisjoint(shock_dict): - results.update(f.diff(shock_dict, h, hide_zeros)) + if outputs is None or not f.outputs.isdisjoint(outputs): + results.update(f.diff(shock_dict, h, hide_zeros)) + if outputs is not None: + results = {k: results[k] for k in outputs if k in results} return results - def diff2(self, shock_dict, h=None, hide_zeros=False): + def diff2(self, shock_dict, h=None, outputs=None, hide_zeros=False): results = {} for f in self.diff_functions.values(): if not f.inputs.isdisjoint(shock_dict): - results.update(f.diff2(shock_dict, h, hide_zeros)) + if outputs is None or not f.outputs.isdisjoint(outputs): + results.update(f.diff2(shock_dict, h, hide_zeros)) + if outputs is not None: + results = {k: results[k] for k in outputs if k in results} return results diff --git a/tests/utils/test_function.py b/tests/utils/test_function.py index 8364164..510de27 100644 --- a/tests/utils/test_function.py +++ b/tests/utils/test_function.py @@ -56,7 +56,19 @@ def test_differentiable_extended_parallel_function(): assert np.isclose(diff['d'], 0) assert np.isclose(diff['f'], 1/16) + # test narrowing down outputs + diff = fs.differentiable(ss1).diff(inputs1, outputs=['c','d']) + assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['d'], 0) + assert list(diff) == ['c', 'd'] + + # if no shocks to first function, hide first function inputs2 = {'e': -2} diff = fs.differentiable(ss1).diff2(inputs2) assert list(diff) == ['f'] assert np.isclose(diff['f'], 1/8) + + # if we ask for output from first function but no inputs shocked, shouldn't be there! + diff = fs.differentiable(ss1).diff(inputs2, outputs=['c', 'f']) + assert list(diff) == ['f'] + assert np.isclose(diff['f'], 1/8) From 7bf97c96b720c51c9af532031217e39da9525aca Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 27 Aug 2021 15:36:12 -0500 Subject: [PATCH 241/288] new hetinputs and hetoutputs implemented with ability to have multiple of each, old tests pass but new functionality not yet tested --- src/sequence_jacobian/blocks/het_block.py | 201 +++++++++----------- src/sequence_jacobian/models/hank.py | 2 +- src/sequence_jacobian/models/two_asset.py | 4 +- src/sequence_jacobian/utilities/function.py | 24 +++ tests/base/test_workflow.py | 4 +- 5 files changed, 115 insertions(+), 120 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index d7fdc5a..c29ea48 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -1,5 +1,6 @@ import copy import numpy as np +from typing import Optional from .support.impulse import ImpulseDict from .support.bijection import Bijection @@ -8,7 +9,8 @@ from ..steady_state.classes import SteadyStateDict from ..jacobian.classes import JacobianDict from .support.bijection import Bijection -from ..utilities.function import DifferentiableExtendedFunction, ExtendedFunction +from ..utilities.function import ExtendedFunction, ExtendedParallelFunction +from ..utilities.ordered_set import OrderedSet def het(exogenous, policy, backward, backward_init=None): @@ -26,7 +28,7 @@ class HetBlock(Block): of the `policy` and non-aggregate output variables specified in the backward step function. """ - def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None): + def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None, hetinputs=None, hetoutputs=None): """Construct HetBlock from backward iteration function. Parameters @@ -57,43 +59,32 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.name = back_step_fun.__name__ super().__init__() - # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. - # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) self.back_step_fun = ExtendedFunction(back_step_fun) - # See the docstring of HetBlock for details on the attributes directly below self.exogenous = exogenous - self.policy, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (policy, backward)) - - # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" - # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun - # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to - # the primed key name, such that self.back_step_fun will properly call those variables. - # e.g. the key "Va" will become "Va_p", associated to the same value. - self.inputs_to_be_primed = {self.exogenous} | set(self.back_iter_vars) - - # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward - # iteration variables themselves. - self.non_back_iter_outputs = self.back_step_fun.outputs - set(self.back_iter_vars) - - # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used - # in utils.graph.block_sort to topologically sort blocks along the DAG - # according to their aggregate outputs and inputs. - # TODO: go back from capitalize to upper!!! (ask Michael first) - self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} + self.policy, self.back_iter_vars = (OrderedSet(utils.misc.make_tuple(x)) for x in (policy, backward)) + self.inputs_to_be_primed = self.back_iter_vars | [self.exogenous] + self.non_back_iter_outputs = self.back_step_fun.outputs - self.back_iter_vars + + self.outputs = OrderedSet([o.capitalize() for o in self.non_back_iter_outputs]) self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) - self.inputs = self.back_step_fun.inputs - {k + '_p' for k in self.back_iter_vars} + self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] self.inputs.remove(exogenous + '_p') self.inputs.add(exogenous) + self.internal = OrderedSet(['D', self.exogenous]) | self.back_step_fun.outputs + + # store "original" copies of these for use whenever we process new hetinputs/hetoutputs + self.original_inputs = self.inputs + self.original_outputs = self.outputs + self.original_internal = self.internal + self.original_M_outputs = self.M_outputs # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. - self.hetinput = None - self.hetoutput = None - - # The set of variables that will be wrapped in a separate namespace for this HetBlock - # as opposed to being available at the top level - self.internal = utils.misc.smart_set(self.back_step_fun.outputs) | utils.misc.smart_set(self.exogenous) | {"D"} + self.hetinputs = hetinputs + self.hetoutputs = hetoutputs + if hetinputs is not None or hetoutputs is not None: + self.process_hetinputs_hetoutputs(hetinputs, hetoutputs, tocopy=False) if len(self.policy) > 2: raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") @@ -127,8 +118,8 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non def __repr__(self): """Nice string representation of HetBlock for printing to console""" - if self.hetinput is not None: - if self.hetoutput is not None: + if self.hetinputs is not None: + if self.hetoutputs is not None: return f"" else: @@ -202,13 +193,13 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, ss.update({"D": D}) # run hetoutput if it's there - if self.hetoutput is not None: - ss.update(self.hetoutput(ss)) + if self.hetoutputs is not None: + ss.update(self.hetoutputs(ss)) # aggregate all outputs other than backward variables on grid, capitalize toreturn = self.non_back_iter_outputs - if self.hetoutput is not None: - toreturn = toreturn | self.hetoutput.outputs + if self.hetoutputs is not None: + toreturn = toreturn | self.hetoutputs.outputs aggregates = {o.capitalize(): np.vdot(D, ss[o]) for o in toreturn} ss.update(aggregates) @@ -250,8 +241,8 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind # allocate empty arrays to store result, assume all like D toreturn = self.non_back_iter_outputs - if self.hetoutput is not None: - toreturn = toreturn | self.hetoutput.outputs + if self.hetoutputs is not None: + toreturn = toreturn | self.hetoutputs.outputs individual_paths = {k: np.empty((T,) + D.shape) for k in toreturn} # backward iteration @@ -264,8 +255,8 @@ def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnind individual.update(self.back_step_fun(individual)) backdict.update({k: individual[k] for k in self.back_iter_vars}) - if self.hetoutput is not None: - individual.update(self.hetoutput(individual)) + if self.hetoutputs is not None: + individual.update(self.hetoutputs(individual)) for k in individual_paths: individual_paths[k][t, ...] = individual[k] @@ -320,7 +311,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): outputs = self.M_outputs.inv @ outputs # horrible # step 0: preliminary processing of steady state - Pi, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) + Pi, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i @@ -329,7 +320,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, ss['D'], Pi.T.copy(), sspol_i, sspol_pi, sspol_space, T, - differentiable_hetinput, differentiable_hetoutput) + differentiable_hetinputs, differentiable_hetoutputs) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o @@ -351,68 +342,52 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): return JacobianDict(J, name=self.name, T=T) - def add_hetinput(self, hetinput, overwrite=False, verbose=True): - # TODO: serious violation, this is mutating the block - """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetinput function. + """HetInput and HetOutput processing""" - A `hetinput` is any non-scalar-valued input argument provided to the HetBlock's backward iteration function, - self.back_step_fun, which is of the same dimensions as the distribution of agents in the HetBlock over - the relevant idiosyncratic state variables, generally referred to as `D`. e.g. The one asset HANK model - example provided in the models directory of sequence_jacobian has a hetinput `T`, which is skill-specific - transfers. - """ - if self.hetinput is not None and overwrite is False: - raise ValueError('Trying to attach hetinput when one already exists!') - else: - if verbose: - if self.hetinput is not None and overwrite is True: - print(f"Overwriting current hetinput, {self.hetinput.__name__} with new hetinput," - f" {hetinput.__name__}!") - else: - print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") - - self.hetinput = ExtendedFunction(hetinput) - self.inputs |= self.hetinput.inputs - self.inputs -= self.hetinput.outputs - - self.internal |= self.hetinput.outputs - - def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): - """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetoutput function. - - A `hetoutput` is any *non-scalar-value* output that the user might desire to be calculated from - the output arguments of the HetBlock's backward iteration function. Importantly, as of now the `hetoutput` - cannot be a function of time displaced values of the HetBlock's outputs but rather must be able to - be calculated from the outputs statically. e.g. The two asset HANK model example provided in the models - directory of sequence_jacobian has a hetoutput, `chi`, the adjustment costs for any initial level of assets - `a`, to any new level of assets `a'`. - """ - if self.hetoutput is not None and overwrite is False: - raise ValueError('Trying to attach hetoutput when one already exists!') + def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunction], hetoutputs: Optional[ExtendedParallelFunction], tocopy=True): + if tocopy: + self = copy.copy(self) + inputs = self.original_inputs.copy() + outputs = self.original_outputs.copy() + internal = self.original_internal.copy() + + if hetoutputs is not None: + inputs |= (hetoutputs.inputs - self.back_step_fun.outputs - ['D']) + outputs |= [o.capitalize() for o in hetoutputs.outputs] + self.M_outputs = Bijection({o: o.capitalize() for o in hetoutputs.outputs}) @ self.original_M_outputs + internal |= hetoutputs.outputs + + if hetinputs is not None: + inputs |= hetinputs.inputs + inputs -= hetinputs.outputs + internal |= hetinputs.outputs + + self.inputs = inputs + self.outputs = outputs + self.internal = internal + + self.hetinputs = hetinputs + self.hetoutputs = hetoutputs + + return self + + def add_hetinputs(self, functions): + if self.hetinputs is None: + return self.process_hetinputs_hetoutputs(ExtendedParallelFunction(functions), self.hetoutputs) else: - if verbose: - if self.hetoutput is not None and overwrite is True: - print(f"Overwriting current hetoutput, {self.hetoutput.name} with new hetoutput," - f" {hetoutput.name}!") - else: - print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") + return self.process_hetinputs_hetoutputs(self.hetinputs.add(functions), self.hetoutputs) - self.hetoutput = ExtendedFunction(hetoutput) + def remove_hetinputs(self, names): + return self.process_hetinputs_hetoutputs(self.hetinputs.remove(names), self.hetoutputs) - # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput - # and aggregating the hetoutput, but do not include: - # 1) objects computed within the HetBlock's backward iteration that enter into the hetoutput computation - # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) - # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation - # but is computed after the backward iteration - self.inputs |= (self.hetoutput.inputs - self.hetinput.outputs - self.back_step_fun.outputs - self.hetoutput.outputs - set("D")) - # Modify the HetBlock's outputs to include the aggregated hetoutputs - self.outputs |= set([o.capitalize() for o in self.hetoutput.outputs]) - self.M_outputs = Bijection({o: o.capitalize() for o in self.hetoutput.outputs}) @ self.M_outputs + def add_hetoutputs(self, functions): + if self.hetoutputs is None: + return self.process_hetinputs_hetoutputs(self.hetinputs, ExtendedParallelFunction(functions)) + else: + return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.add(functions)) - self.internal |= self.hetoutput.outputs + def remove_hetoutputs(self, names): + return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.remove(names)) '''Part 3: components of ss(): - policy_ss : backward iteration to get steady-state policies and other outcomes @@ -465,8 +440,8 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): for k in self.inputs_to_be_primed: ssin[k] = ssin[k + '_p'] del ssin[k + '_p'] - if self.hetinput is not None: - for k in self.hetinput.inputs: + if self.hetinputs is not None: + for k in self.hetinputs.inputs: if k in original_ssin: ssin[k] = original_ssin[k] return {**ssin, **sspol} @@ -557,11 +532,7 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step def backward_iteration_fakenews(self, input_shocked, output_list, differentiable_back_step_fun, Dss, Pi_T, sspol_i, sspol_pi, sspol_space, T, differentiable_hetinput, differentiable_hetoutput): """Iterate policy steps backward T times for a single shock.""" - # TODO: Might need to add a check for ss_for_hetinput if self.hetinput is not None - # since unless self.hetinput_inputs is exactly equal to input_shocked, calling - # self.hetinput() inside the symmetric differentiation function will throw an error. - # It's probably better/more informative to throw that error out here. - if self.hetinput is not None and input_shocked in self.hetinput.inputs: + if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: # if input_shocked is an input to hetinput, take numerical diff to get response din_dict = differentiable_hetinput.diff2({input_shocked: 1}) else: @@ -646,14 +617,14 @@ def jac_prelim(self, ss, h): grid = {k: ss[k+'_grid'] for k in self.policy} differentiable_back_step_fun = self.back_step_fun.differentiable(self.make_inputs(ss), h=h) - differentiable_hetinput = None - if self.hetinput is not None: + differentiable_hetinputs = None + if self.hetinputs is not None: # ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} - differentiable_hetinput = self.hetinput.differentiable(ss) + differentiable_hetinputs = self.hetinputs.differentiable(ss) - differentiable_hetoutput = None - if self.hetoutput is not None: - differentiable_hetoutput = self.hetoutput.differentiable(ss) + differentiable_hetoutputs = None + if self.hetoutputs is not None: + differentiable_hetoutputs = self.hetoutputs.differentiable(ss) # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints sspol_i = {} @@ -664,7 +635,7 @@ def jac_prelim(self, ss, h): sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss[pol]) sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - return Pi, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, sspol_i, sspol_pi, sspol_space + return Pi, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, sspol_i, sspol_pi, sspol_space '''Part 6: helper to extract inputs and potentially process them through hetinput''' @@ -677,8 +648,8 @@ def make_inputs(self, back_step_inputs_dict): else: input_dict = back_step_inputs_dict.copy() - if self.hetinput is not None: - input_dict.update(self.hetinput(input_dict)) + if self.hetinputs is not None: + input_dict.update(self.hetinputs(input_dict)) if not all(k in input_dict for k in self.back_iter_vars): input_dict.update(self.backward_init(input_dict)) diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py index a38df0e..393967c 100644 --- a/src/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -64,7 +64,7 @@ def transfers(pi_e, Div, Tax, e_grid): return T -household.add_hetinput(transfers, verbose=False) +household = household.add_hetinputs([transfers]) @njit diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index d9e7fc2..99c6fc6 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -121,8 +121,8 @@ def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): return chi -household.add_hetinput(income, verbose=False) -household.add_hetoutput(adjustment_costs, verbose=False) +household = household.add_hetinputs([income]) +household = household.add_hetoutputs([adjustment_costs]) """Supporting functions for HA block""" diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 1fb77eb..53a50b2 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -75,6 +75,9 @@ def __call__(self, input_dict): input_dict = {k: v for k, v in input_dict.items() if k in self.inputs} return self.outputs.dict_from(make_tuple(self.f(**input_dict))) + def __repr__(self): + return f'<{type(self).__name__}({self.name}): {self.inputs} -> {self.outputs}>' + def wrapped_call(self, input_dict, preprocess=None, postprocess=None): if preprocess is not None: input_dict = {k: preprocess(v) for k, v in input_dict.items() if k in self.inputs} @@ -99,6 +102,7 @@ def __init__(self, f, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4): self.h = h self.h2 = h2 + def diff(self, shock_dict, h=None, hide_zeros=False): if h is None: h = self.h @@ -153,6 +157,9 @@ def __init__(self, fs, name=None): raise ValueError(f'Overlap in outputs of ParallelFunction: {ext_f.name} and others both have {outputs & ext_f.outputs}') inputs |= ext_f.inputs outputs |= ext_f.outputs + + if ext_f.name in functions: + raise ValueError(f'Overlap in function names of ParallelFunction: {ext_f.name} listed twice') functions[ext_f.name] = ext_f self.inputs = inputs @@ -191,6 +198,23 @@ def call_on_deviations(self, ss, dev_dict, outputs=None): def wrapped_call(self, input_dict, preprocess=None, postprocess=None): raise NotImplementedError + def add(self, f): + if isinstance(f, function) or isinstance(f, ExtendedFunction): + return ExtendedParallelFunction(list(self.functions.values()) + [f]) + else: + # otherwise assume f is iterable + return ExtendedParallelFunction(list(self.functions.values()) + list(f)) + + def remove(self, name): + if isinstance(name, str): + return ExtendedParallelFunction([v for k, v in self.functions.items() if k != name]) + else: + # otherwise assume name is iterable + return ExtendedParallelFunction([v for k, v in self.functions.items() if k not in name]) + + def children(self): + return OrderedSet(self.functions) + def differentiable(self, input_dict, h=1E-6, h2=1E-4): return DifferentiableExtendedParallelFunction(self.functions, self.name, self.inputs, self.outputs, input_dict, h, h2) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 9546eec..7f0a3eb 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -91,8 +91,8 @@ def mpcs(c, a, a_grid, rpost): return mpc -household.add_hetinput(income, verbose=False) -household.add_hetoutput(mpcs, verbose=False) +household = household.add_hetinputs([income]) +household = household.add_hetoutputs([mpcs]) '''Part 2: rest of the model''' From 20680f3ba97a0d7cfb0ecfe434eed72c52952a94 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 27 Aug 2021 16:56:32 -0400 Subject: [PATCH 242/288] Cleaned up vacuous Js input of .impulse_nonlinear of leaf blocks --- src/sequence_jacobian/blocks/het_block.py | 2 +- src/sequence_jacobian/blocks/simple_block.py | 2 +- src/sequence_jacobian/primitives.py | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index c29ea48..ccf57dc 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -206,7 +206,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, {self.name: {k: ss[k] for k in ss if k in self.internal}}) - def _impulse_nonlinear(self, ss, inputs, outputs, Js, monotonic=False, returnindividual=False, grid_paths=None): + def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindividual=False, grid_paths=None): """Evaluate transitional dynamics for HetBlock given dynamic paths for `inputs`, assuming that we start and end in steady state `ss`, and that all inputs not specified in `inputs` are constant at their ss values. diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 13181cd..21dcc3e 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -48,7 +48,7 @@ def _steady_state(self, ss): outputs = self.f.wrapped_call(ss, preprocess=ignore, postprocess=misc.numeric_primitive) return SteadyStateDict({**ss, **outputs}) - def _impulse_nonlinear(self, ss, inputs, outputs, Js): + def _impulse_nonlinear(self, ss, inputs, outputs): input_args = {} for k, v in inputs.items(): if np.isscalar(v): diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index a6062c8..b9cf19a 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -91,12 +91,16 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> ImpulseDict: - # TODO: do we want Js at all? better alternative to kwargs? """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" inputs = ImpulseDict(inputs) _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **kwargs) + + # SolvedBlocks may use Js and may be nested in a CombinedBlock + if isinstance(self, Parent): + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **kwargs) + else: + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **kwargs) def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, @@ -248,7 +252,6 @@ def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): def remap(self, map): other = deepcopy(self) other.M = self.M @ Bijection(map) - # TODO: maybe we want to have an ._inputs and ._outputs that never changes, so that it can be used internally? other.inputs = other.M @ self.inputs other.outputs = other.M @ self.outputs if hasattr(self, 'input_list'): From 6c4138dcd64f3b3d38bd718ce3ed09e7bba1494b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 27 Aug 2021 17:53:56 -0400 Subject: [PATCH 243/288] Test parallel hetinputs in est_workflow. Minor generalization of het_block which assumed that Pi or grid are not generated by hetinputs. --- src/sequence_jacobian/blocks/het_block.py | 29 ++++++++++++----------- tests/base/test_workflow.py | 15 ++++-------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index ccf57dc..5eaaef4 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -177,10 +177,12 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, """ ss = calibration.toplevel.copy() + if self.hetinputs is not None: + ss.update(self.hetinputs(ss)) # extract information from calibration Pi = ss[self.exogenous] - grid = {k: ss[k+'_grid'] for k in self.policy} + grid = {k: ss[k + '_grid'] for k in self.policy} D_seed = ss.get('D', None) pi_seed = ss.get(self.exogenous + '_seed', None) @@ -223,9 +225,10 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid grid_paths: [optional] dict of {str: array(T, Number of grid points)} time-varying grids for policies """ + ssin_dict = {**ss.toplevel, **ss.internal[self.name]} + Pi_T = ssin_dict[self.exogenous].T.copy() + D = ssin_dict['D'] T = inputs.T - Pi_T = ss.internal[self.name][self.exogenous].T.copy() - D = ss.internal[self.name]['D'] # construct grids for policy variables either from the steady state grid if the grid is meant to be # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. @@ -236,7 +239,7 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid grid[k] = grid_paths[k] use_ss_grid[k] = False else: - grid[k] = ss[k + "_grid"] + grid[k] = ssin_dict[k + "_grid"] use_ss_grid[k] = True # allocate empty arrays to store result, assume all like D @@ -246,11 +249,12 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid individual_paths = {k: np.empty((T,) + D.shape) for k in toreturn} # backward iteration - backdict = dict(ss.items()) - backdict.update(copy.deepcopy(ss.internal[self.name])) + backdict = dict(ssin_dict.items()) for t in reversed(range(T)): # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) + backdict.update({k: ssin_dict[k] + v[t, ...] for k, v in inputs.items()}) + if self.hetinputs is not None: + backdict.update(self.hetinputs(backdict)) individual = self.make_inputs(backdict) individual.update(self.back_step_fun(individual)) backdict.update({k: individual[k] for k in self.back_iter_vars}) @@ -308,6 +312,8 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): h for numerical differentiation of backward iteration """ ss = {**ss.toplevel, **ss.internal[self.name]} + if self.hetinputs is not None: + ss.update(self.hetinputs(ss)) outputs = self.M_outputs.inv @ outputs # horrible # step 0: preliminary processing of steady state @@ -412,7 +418,7 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): all steady-state outputs of backward iteration, combined with inputs to backward iteration """ - # find initial values for backward iteration and account for hetinputs + # find initial values for backward iteration original_ssin = ssin ssin = self.make_inputs(ssin) @@ -640,17 +646,12 @@ def jac_prelim(self, ss, h): '''Part 6: helper to extract inputs and potentially process them through hetinput''' def make_inputs(self, back_step_inputs_dict): - """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, - process stuff through self.hetinput first if it's there. - """ + """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun.""" if isinstance(back_step_inputs_dict, SteadyStateDict): input_dict = {**back_step_inputs_dict.toplevel, **back_step_inputs_dict.internal[self.name]} else: input_dict = back_step_inputs_dict.copy() - if self.hetinputs is not None: - input_dict.update(self.hetinputs(input_dict)) - if not all(k in input_dict for k in self.back_iter_vars): input_dict.update(self.backward_init(input_dict)) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 7f0a3eb..c961f1b 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -8,8 +8,7 @@ def household_init(a_grid, y, rpost, sigma): - c = np.maximum(1e-8, y[:, np.newaxis] + - np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) + c = np.maximum(1e-8, y[:, np.newaxis] + np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) Va = (1 + rpost) * (c ** (-sigma)) return Va @@ -66,20 +65,16 @@ def get_mpcs(c, a, a_grid, rpost): def income(tau, Y, e_grid, e_dist, Gamma, transfer): """Labor income on the grid.""" - gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, - e_grid ** (1 + Gamma * np.log(Y))) + gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, e_grid ** (1 + Gamma * np.log(Y))) y = (1 - tau) * Y * gamma * e_grid + transfer return y - @simple def income_state_vars(rho_e, sd_e, nE): - e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst( - rho=rho_e, sigma=sd_e, N=nE) + e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) return e_grid, e_dist, Pi -@simple def asset_state_vars(amin, amax, nA): a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) return a_grid @@ -91,7 +86,7 @@ def mpcs(c, a, a_grid, rpost): return mpc -household = household.add_hetinputs([income]) +household = household.add_hetinputs([income, asset_state_vars]) household = household.add_hetoutputs([mpcs]) @@ -139,7 +134,7 @@ def mkt_clearing(A, B, C, Y, G): def test_all(): - hh = combine([household, income_state_vars, asset_state_vars], name='HH') + hh = combine([household, income_state_vars], name='HH') calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143, 'rho_B': 0.8} From 286fcbbb1cf51b425a6fba11eee42f5ff6b80d22 Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 31 Aug 2021 14:31:13 -0500 Subject: [PATCH 244/288] WIP commit with new implementation of SSCalibrationBlock. Preliminarily working --- .../auxiliary_blocks/redirected_block.py | 84 ------- .../auxiliary_blocks/ss_calibration_block.py | 215 ++++++++++++++++++ .../blocks/combined_block.py | 9 +- src/sequence_jacobian/primitives.py | 15 +- src/sequence_jacobian/steady_state/drivers.py | 5 - src/sequence_jacobian/utilities/graph.py | 90 ++------ 6 files changed, 239 insertions(+), 179 deletions(-) delete mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py create mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py deleted file mode 100644 index da1818d..0000000 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/redirected_block.py +++ /dev/null @@ -1,84 +0,0 @@ -"""An auxiliary Block class for altering the direction of some of the equations in a given Block object""" - -import numpy as np - -from ..combined_block import CombinedBlock -from ..parent import Parent -from ...primitives import Block -from ...utilities.ordered_set import OrderedSet - - -class RedirectedBlock(CombinedBlock): - """A RedirectedBlock is a Block where a subset of the input-output mappings are altered, or 'redirected'. - This is useful when computing steady states in particular, where often we will want to set target values - explicitly and back out what are the implied values of the unknowns that justify those targets.""" - def __init__(self, block, redirect_block): - Block.__init__(self) - - # TODO: Figure out what are the criteria we want to require of the helper block - # if not redirect_block.inputs & block.outputs: - # raise ValueError("User-provided redirect_block must ") - # assert redirect_block.outputs <= (block.inputs | block.outputs) - self.directed = block - # TODO: Implement case with multiple redirecting (helper) blocks later. - # If `block` is a non-nested Block then multiple helper blocks may seem a bit redundant, since - # helper blocks should typically be sorted before the `block` they are associated to (CHECK THIS), - # but multiple helper blocks may be necessary when `block` is a nested Block, i.e. if helpers need to - # be inserted at different stages of the DAG. Then we also need to do some non-trivial sorting when - # filling out the self.blocks attribute - self.redirected = redirect_block - self.blocks = [redirect_block, block] - - self.name = block.name + "_redirect" - - # now that it has a name, do Parent initialization - Parent.__init__(self, [block, redirect_block]) - - self.inputs = (redirect_block.inputs | block.inputs) - redirect_block.outputs - self.outputs = (redirect_block.outputs | block.outputs) - redirect_block.inputs - - # TODO: This all may not be necessary so long as there is a function that undoes the - # insertion of all of the redirect blocks and re-sorts the DAG (if necessary) - # # Calculate what are the inputs and outputs of the Block objects underlying `self`, without - # # any of the redirecting blocks. - # # TODO: Think more carefully about evaluating a DAG with bypass_redirection, if the redirecting - # # blocks change the sort order of the DAG!! - # if not isinstance(self.directed, Parent): - # self.inputs_directed = self.directed.inputs - # self.outputs_directed = self.directed.outputs - # else: - # inputs_directed, outputs_directed = OrderedSet({}), OrderedSet({}) - # ps_checked = set({}) - # for d in self.directed.descendants: - # # The descendant's parent's name (if it has one, o/w the descendant's name) - # p = self.directed.descendants[d] - # if p is None or p in ps_checked: - # continue - # else: - # ps_checked |= set(p) - # if hasattr(self.directed[p], "directed"): - # inputs_directed |= self.directed[p].directed.inputs - # outputs_directed |= self.directed[p].directed.outputs - # else: - # inputs_directed |= self[d].inputs - # outputs_directed |= self[d].outputs - # self.inputs_directed = inputs_directed - outputs_directed - # self.outputs_directed = outputs_directed - - def __repr__(self): - return f"" - - def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): - """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" - if bypass_redirection: - kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection - return self.directed.steady_state(calibration, **{k: v for k, v in kwargs.items() if k in self.directed.ss_valid_input_kwargs}) - else: - ss = calibration.copy() - for block in self.blocks: - # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, **kwargs) - # If we're not bypassing redirection, don't overwrite entries in ss populated by the redirect block - ss.update(outputs) if block == self.redirected else ss.update(outputs.difference(ss)) - return ss diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py new file mode 100644 index 0000000..79af1a4 --- /dev/null +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py @@ -0,0 +1,215 @@ +"""A wrapper for Block, which incorporates additional blocks useful for steady state calibration""" + +import numpy as np + +from ..combined_block import CombinedBlock +from ...steady_state.drivers import _solve_for_unknowns +from ...steady_state.support import compute_target_values +from ...utilities.misc import smart_zip +from ...utilities.graph import topological_sort +from ...utilities.ordered_set import OrderedSet + + +class SSCalibrationBlock(CombinedBlock): + """An SSCalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering + the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an + SSCalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" + def __init__(self, blocks, helper_blocks, calibration, name=""): + sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) + intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) + + super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) + + self.helper_blocks = helper_blocks + self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) + + self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) + self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig + + def __repr__(self): + return f"" + + def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): + """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" + ss = calibration.copy() + helper_outputs = {} + for block in self.blocks: + if not evaluate_helpers and block in self.helper_blocks: + continue + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) + if evaluate_helpers and block in self.helper_blocks: + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) + ss.update(outputs) + else: + # Don't overwrite entries in ss_values corresponding to what has already + # been solved for in helper_blocks so we can check for consistency after-the-fact + ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) + return ss + + def solve_steady_state(self, calibration, unknowns, targets, dissolve=[], solver=None, solver_kwargs={}, + block_kwargs={}, ttol=1e-9, ctol=1e-9, helper_targets={}, verbose=False, + check_consistency=True, constrained_method="linear_continuation", constrained_kwargs={}): + ss = calibration.copy() + def residual(unknown_values, evaluate_helpers=True): + ss.update(smart_zip(unknowns.keys(), unknown_values)) + ss.update(self.steady_state(ss, dissolve=dissolve, helper_targets=helper_targets, + evaluate_helpers=evaluate_helpers, **block_kwargs)) + return compute_target_values(targets, ss) + unknowns_solved = _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, tol=ttol, verbose=verbose) + if check_consistency: + # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers + unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) + cresid = np.max(abs(residual(unknowns_solved.values(), evaluate_helpers=False))) + if cresid > ctol: + raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" + f" the consistency of the DAG without redirection.") + return ss + + +def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): + """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. + + Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's + inferred) that indicate their aggregate inputs and outputs + + Importantly, because including helper blocks in a blocks without additional measures + can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the + steady_state computation to resolve these cycles. + e.g. Consider Krusell Smith: + Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the helper block + because the "firm" block outputs "r", which the helper block takes as an input. + However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes + "K" as an input. + This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then + "firm" could be removed as a dependency of helper block and the cycle would be resolved. + + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + helper_blocks: `list` + A list of helper blocks + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using helper blocks. Read above docstring for more detail + return_io: `bool` + A boolean indicating whether to return the full set of input and output arguments from `blocks` + """ + # TODO: Decide whether we want to break out the input and output argument tracking and return to + # a different function... currently it's very convenient to slot it into block_sort directly, but it + # does clutter up the function body + if return_io: + # step 1: map outputs to blocks for topological sort + outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, + helper_blocks=helper_blocks, calibration=calibration) + + # step 2: dependency graph for topological sort and input list + dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, + helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep), inargs, outargs + else: + # step 1: map outputs to blocks for topological sort + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) + + # step 2: dependency graph for topological sort and input list + dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep) + + +def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): + """Mirroring construct_output_map functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + + helper_inputs = set().union(*[block.inputs for block in helper_blocks]) + + outmap = dict() + outargs = set() + for num, block in enumerate(blocks): + # Find the relevant set of outputs corresponding to a block + if hasattr(block, "outputs"): + outputs = block.outputs + elif isinstance(block, dict): + outputs = block.keys() + else: + raise ValueError(f'{block} is not recognized as block or does not provide outputs') + + for o in outputs: + # Because some of the outputs of a helper block are, by construction, outputs that also appear in the + # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering + # throwing this ValueError + if o in outmap and block not in helper_blocks: + raise ValueError(f'{o} is output twice') + + # Priority sorting for standard blocks: + # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share + # a given output, such that the dependency graph is constructed on the standard blocks, where possible + if o not in outmap: + outmap[o] = num + if return_output_args and not (o in helper_inputs and o in calibration): + outargs.add(o) + else: + continue + if return_output_args: + return outmap, outargs + else: + return outmap + + +def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, + calibration=None, return_input_args=False, outargs=None): + """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + if outargs is None: + outargs = {} + + dep = {num: set() for num in range(len(blocks))} + inargs = set() + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + # Each potential input to a given block will either be 1) output by another block, + # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into + # the steady-state computation via the `calibration' dict. + # If the block is a helper block, then we want to check the calibration to see if the potential + # input is a pre-specified variable/parameter, and if it is then we will not add the block that + # produces that input as an output as a dependency. + # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic + # dependency, if it were not for this resolution. + if i in outmap and not (i in calibration and block in helper_blocks): + dep[num].add(outmap[i]) + if return_input_args and not (i in outargs): + inargs.add(i) + if return_input_args: + return dep, inargs + else: + return dep + + +def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): + """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module + but augmented to support helper blocks""" + required = set() + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + if i in outmap: + required.add(i) + return required diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index c23cf7e..1b4ff61 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -5,12 +5,11 @@ from .support.impulse import ImpulseDict from ..primitives import Block -from .. import utilities as utils +from ..utilities.graph import block_sort, find_intermediate_inputs from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from ..blocks.parent import Parent from ..steady_state.support import provide_solver_default from ..jacobian.classes import JacobianDict -from ..steady_state.classes import SteadyStateDict def combine(blocks, name="", model_alias=False): @@ -29,12 +28,12 @@ class CombinedBlock(Block, Parent): # To users: Do *not* manually change the attributes via assignment. Instantiating a # CombinedBlock has some automated features that are inferred from initial instantiation but not from # re-assignment of attributes post-instantiation. - def __init__(self, blocks, name="", model_alias=False): + def __init__(self, blocks, name="", model_alias=False, sorted_indices=None, intermediate_inputs=None): super().__init__() self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] - self._sorted_indices = utils.graph.block_sort(blocks) - self._required = utils.graph.find_outputs_that_are_intermediate_inputs(blocks) + self._sorted_indices = block_sort(blocks) if sorted_indices is None else sorted_indices + self._required = find_intermediate_inputs(blocks) if intermediate_inputs is None else intermediate_inputs self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] if not name: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index d6f9144..059bb1c 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -75,18 +75,13 @@ def outputs(self): pass def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], - dissolve: Optional[List[str]] = [], bypass_redirection: bool = False, + dissolve: Optional[List[str]] = [], evaluate_helpers: bool = False, **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" - # Special handling: 1) Find inputs/outputs of the Block w/o redirection + # Special handling: 1) Find inputs/outputs of the Block w/o helpers blocks # 2) Add all unknowns of dissolved blocks to inputs - if bypass_redirection and isinstance(self, Parent): - if hasattr(self, "inputs_directed"): - inputs = self.directed.inputs.copy() - else: - inputs = OrderedSet({}) - for d in self.descendants: - inputs |= self[d].inputs_directed.copy() if hasattr(self, "inputs_directed") else self[d].inputs.copy() + if not evaluate_helpers: + inputs = self.inputs_orig.copy() if hasattr(self, "inputs_orig") else self.inputs.copy() else: inputs = self.inputs.copy() if isinstance(self, Parent): @@ -94,7 +89,7 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], inputs |= self.get_attribute(k, 'unknowns').keys() calibration = SteadyStateDict(calibration)[inputs] - kwargs['dissolve'], kwargs['bypass_redirection'] = dissolve, bypass_redirection + kwargs['dissolve'], kwargs['evaluate_helpers'] = dissolve, evaluate_helpers return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py index a6a9111..f5c0285 100644 --- a/src/sequence_jacobian/steady_state/drivers.py +++ b/src/sequence_jacobian/steady_state/drivers.py @@ -238,11 +238,6 @@ def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, residual_kwar unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, tol=tol, verbose=verbose, **solver_kwargs) unknown_solutions = list(unknown_solutions) - elif solver == "solved": - # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution - # simply call residual_f once to populate the `ss_values` dict - residual_f(unknowns.values()) - unknown_solutions = unknowns.values() else: raise RuntimeError(f"steady_state is not yet compatible with {solver}.") diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index 456f033..c091f70 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -1,34 +1,14 @@ """Topological sort and related code""" -def block_sort(blocks, redirect_blocks=None, calibration=None, return_io=False): +def block_sort(blocks, return_io=False): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's inferred) that indicate their aggregate inputs and outputs - Importantly, because including redirect blocks in a blocks without additional measures - can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the - steady_state computation to resolve these cycles. - e.g. Consider Krusell Smith: - Suppose one specifies a redirect block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the redirect block - because the "firm" block outputs "r", which the redirect block takes as an input. - However, it would also include the redirect block as a dependency of the "firm" block because the "firm" block takes - "K" as an input. - This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of redirect block and the cycle would be resolved. - blocks: `list` A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - ignore_redirects: `bool` - A boolean indicating whether to account for/return the indices of redirect blocks contained in blocks - Set to true when sorting for td and jac calculations - redirect_indices: `list` - A list of indices corresponding to the redirect blocks in the blocks - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using redirect blocks. Read above docstring for more detail return_io: `bool` A boolean indicating whether to return the full set of input and output arguments from `blocks` """ @@ -37,20 +17,18 @@ def block_sort(blocks, redirect_blocks=None, calibration=None, return_io=False): # does clutter up the function body if return_io: # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(blocks, redirect_blocks=redirect_blocks, - return_output_args=True) + outmap, outargs = construct_output_map(blocks, return_output_args=True) # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True, - redirect_blocks=redirect_blocks, calibration=calibration) + dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True) return topological_sort(dep), inargs, outargs else: # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(blocks, redirect_blocks=redirect_blocks) + outmap = construct_output_map(blocks) # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(blocks, outmap, calibration=calibration, redirect_blocks=redirect_blocks) + dep = construct_dependency_graph(blocks, outmap) return topological_sort(dep) @@ -82,23 +60,18 @@ def topological_sort(dep, names=None): return topsorted -def construct_output_map(blocks, redirect_blocks=None, return_output_args=False): +def construct_output_map(blocks, return_output_args=False): """Construct a map of outputs to the indices of the blocks that produce them. blocks: `list` A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - redirect_blocks: `list` - A list of redirect blocks, designed to aid steady state computation, to include in the sort return_output_args: `bool` A boolean indicating whether to track and return the full set of output arguments of all of the blocks in `blocks` """ - if redirect_blocks is None: - redirect_blocks = [] - outmap = dict() outargs = set() - for num, block in enumerate(blocks + redirect_blocks): + for num, block in enumerate(blocks): # Find the relevant set of outputs corresponding to a block if hasattr(block, "outputs"): outputs = block.outputs @@ -108,60 +81,32 @@ def construct_output_map(blocks, redirect_blocks=None, return_output_args=False) raise ValueError(f'{block} is not recognized as block or does not provide outputs') for o in outputs: - # Because some of the outputs of a redirect block are, by construction, outputs that also appear in the - # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering - # throwing this ValueError - if o in outmap and block not in redirect_blocks: + if o in outmap: raise ValueError(f'{o} is output twice') + outmap[o] = num - # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a redirect block if both share - # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap: - outmap[o] = num - if return_output_args: - outargs.add(o) - else: - continue if return_output_args: return outmap, outargs else: return outmap -def construct_dependency_graph(blocks, outmap, redirect_blocks=None, - calibration=None, return_input_args=False): +def construct_dependency_graph(blocks, outmap, return_input_args=False): """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where this set is the set of blocks that the key block is dependent on. outmap is the output map (output to block index mapping) created by construct_output_map. - - See the docstring of block_sort for more details about the other arguments. """ - if calibration is None: - calibration = {} - if redirect_blocks is None: - redirect_blocks = [] - dep = {num: set() for num in range(len(blocks + redirect_blocks))} + dep = {num: set() for num in range(len(blocks))} inargs = set() - for num, block in enumerate(blocks + redirect_blocks): + for num, block in enumerate(blocks): if hasattr(block, 'inputs'): inputs = block.inputs else: inputs = set(i for o in block for i in block[o]) for i in inputs: - if return_input_args: - inargs.add(i) - # Each potential input to a given block will either be 1) output by another block, - # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into - # the steady-state computation via the `calibration' dict. - # If the block is a redirect block, then we want to check the calibration to see if the potential - # input is a pre-specified variable/parameter, and if it is then we will not add the block that - # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution redirect block and firm block would create a cyclic - # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in redirect_blocks): + if i in outmap: dep[num].add(outmap[i]) if return_input_args: return dep, inargs @@ -169,17 +114,12 @@ def construct_dependency_graph(blocks, outmap, redirect_blocks=None, return dep -def find_outputs_that_are_intermediate_inputs(blocks, redirect_blocks=None): +def find_intermediate_inputs(blocks, **kwargs): """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. - - See the docstring of construct_output_map for more details about the arguments. """ - if redirect_blocks is None: - redirect_blocks = [] - required = set() - outmap = construct_output_map(blocks, redirect_blocks=redirect_blocks) + outmap = construct_output_map(blocks, **kwargs) for num, block in enumerate(blocks): if hasattr(block, 'inputs'): inputs = block.inputs From 473eeca3e2f1042c358e8a43e47dc3718dcd17ac Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Tue, 31 Aug 2021 15:32:45 -0500 Subject: [PATCH 245/288] Complete implementation of SSCalibrationBlock, remove steady_state .drivers module and shift core solve_steady_state functionality into Block method --- src/sequence_jacobian/__init__.py | 3 - .../auxiliary_blocks/ss_calibration_block.py | 215 -------------- .../blocks/combined_block.py | 223 +++++++++++++- src/sequence_jacobian/blocks/solved_block.py | 6 +- src/sequence_jacobian/primitives.py | 31 +- src/sequence_jacobian/steady_state/drivers.py | 280 ------------------ src/sequence_jacobian/steady_state/support.py | 102 ++++++- 7 files changed, 338 insertions(+), 522 deletions(-) delete mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py delete mode 100644 src/sequence_jacobian/steady_state/drivers.py diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index 72f8d84..835fdf0 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -11,11 +11,8 @@ from .jacobian.classes import JacobianDict from .blocks.support.impulse import ImpulseDict - from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved -from .steady_state.drivers import steady_state - # Useful utilities for setting up HetBlocks from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen from .utilities.interpolate import interpolate_y diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py deleted file mode 100644 index 79af1a4..0000000 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/ss_calibration_block.py +++ /dev/null @@ -1,215 +0,0 @@ -"""A wrapper for Block, which incorporates additional blocks useful for steady state calibration""" - -import numpy as np - -from ..combined_block import CombinedBlock -from ...steady_state.drivers import _solve_for_unknowns -from ...steady_state.support import compute_target_values -from ...utilities.misc import smart_zip -from ...utilities.graph import topological_sort -from ...utilities.ordered_set import OrderedSet - - -class SSCalibrationBlock(CombinedBlock): - """An SSCalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering - the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an - SSCalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" - def __init__(self, blocks, helper_blocks, calibration, name=""): - sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) - intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) - - super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) - - self.helper_blocks = helper_blocks - self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) - - self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) - self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig - - def __repr__(self): - return f"" - - def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): - """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" - ss = calibration.copy() - helper_outputs = {} - for block in self.blocks: - if not evaluate_helpers and block in self.helper_blocks: - continue - # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) - if evaluate_helpers and block in self.helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) - ss.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) - return ss - - def solve_steady_state(self, calibration, unknowns, targets, dissolve=[], solver=None, solver_kwargs={}, - block_kwargs={}, ttol=1e-9, ctol=1e-9, helper_targets={}, verbose=False, - check_consistency=True, constrained_method="linear_continuation", constrained_kwargs={}): - ss = calibration.copy() - def residual(unknown_values, evaluate_helpers=True): - ss.update(smart_zip(unknowns.keys(), unknown_values)) - ss.update(self.steady_state(ss, dissolve=dissolve, helper_targets=helper_targets, - evaluate_helpers=evaluate_helpers, **block_kwargs)) - return compute_target_values(targets, ss) - unknowns_solved = _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, tol=ttol, verbose=verbose) - if check_consistency: - # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers - unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) - cresid = np.max(abs(residual(unknowns_solved.values(), evaluate_helpers=False))) - if cresid > ctol: - raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" - f" the consistency of the DAG without redirection.") - return ss - - -def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): - """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. - - Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's - inferred) that indicate their aggregate inputs and outputs - - Importantly, because including helper blocks in a blocks without additional measures - can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the - steady_state computation to resolve these cycles. - e.g. Consider Krusell Smith: - Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the helper block - because the "firm" block outputs "r", which the helper block takes as an input. - However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes - "K" as an input. - This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of helper block and the cycle would be resolved. - - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - helper_blocks: `list` - A list of helper blocks - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using helper blocks. Read above docstring for more detail - return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `blocks` - """ - # TODO: Decide whether we want to break out the input and output argument tracking and return to - # a different function... currently it's very convenient to slot it into block_sort directly, but it - # does clutter up the function body - if return_io: - # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, - helper_blocks=helper_blocks, calibration=calibration) - - # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, - helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(dep), inargs, outargs - else: - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) - - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(dep) - - -def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): - """Mirroring construct_output_map functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - - helper_inputs = set().union(*[block.inputs for block in helper_blocks]) - - outmap = dict() - outargs = set() - for num, block in enumerate(blocks): - # Find the relevant set of outputs corresponding to a block - if hasattr(block, "outputs"): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: - # Because some of the outputs of a helper block are, by construction, outputs that also appear in the - # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering - # throwing this ValueError - if o in outmap and block not in helper_blocks: - raise ValueError(f'{o} is output twice') - - # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share - # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap: - outmap[o] = num - if return_output_args and not (o in helper_inputs and o in calibration): - outargs.add(o) - else: - continue - if return_output_args: - return outmap, outargs - else: - return outmap - - -def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, - calibration=None, return_input_args=False, outargs=None): - """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - if outargs is None: - outargs = {} - - dep = {num: set() for num in range(len(blocks))} - inargs = set() - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - # Each potential input to a given block will either be 1) output by another block, - # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into - # the steady-state computation via the `calibration' dict. - # If the block is a helper block, then we want to check the calibration to see if the potential - # input is a pre-specified variable/parameter, and if it is then we will not add the block that - # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic - # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in helper_blocks): - dep[num].add(outmap[i]) - if return_input_args and not (i in outargs): - inargs.add(i) - if return_input_args: - return dep, inargs - else: - return dep - - -def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): - """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module - but augmented to support helper blocks""" - required = set() - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - if i in outmap: - required.add(i) - return required diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 1b4ff61..af1a9cd 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -1,14 +1,15 @@ """CombinedBlock class and the combine function to generate it""" from copy import deepcopy -import numpy as np -from .support.impulse import ImpulseDict from ..primitives import Block +from ..utilities.misc import dict_diff from ..utilities.graph import block_sort, find_intermediate_inputs +from ..utilities.graph import topological_sort +from ..utilities.ordered_set import OrderedSet from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from ..blocks.parent import Parent -from ..steady_state.support import provide_solver_default +from ..steady_state.support import subset_helper_block_unknowns from ..jacobian.classes import JacobianDict @@ -60,14 +61,14 @@ def __repr__(self): else: return f"" - def _steady_state(self, calibration, dissolve=[], bypass_redirection=False, **kwargs): + def _steady_state(self, calibration, dissolve=[], **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" ss = deepcopy(calibration) for block in self.blocks: # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, bypass_redirection=bypass_redirection, **kwargs) + outputs = block.steady_state(ss, dissolve=inner_dissolve, **kwargs) ss.update(outputs) return ss @@ -131,16 +132,210 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): return total_Js[original_outputs, :] - def solve_steady_state(self, calibration, unknowns, targets, solver=None, helper_blocks=None, - sort_blocks=False, **kwargs): - if solver is None: - solver = provide_solver_default(unknowns) - if helper_blocks and sort_blocks is False: - sort_blocks = True - - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, - helper_blocks=helper_blocks, sort_blocks=sort_blocks, **kwargs) + def solve_steady_state(self, calibration, unknowns, targets, solver="", helper_blocks=None, helper_targets=None, + **kwargs): + if helper_blocks is not None: + if helper_targets is None: + raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" + " in the `helper_targets` keyword argument.") + else: + targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + helper_targets = {t: targets[t] for t in helper_targets} if isinstance(helper_targets, list) else helper_targets + helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) + + calibration_w_help = calibration.copy() + calibration_w_help.update(helper_targets) + ss_calibration_block = SSCalibrationBlock(self.blocks + helper_blocks, helper_blocks=helper_blocks, + calibration=calibration_w_help) + return ss_calibration_block.solve_steady_state(calibration_w_help, dict_diff(unknowns, helper_unknowns), + dict_diff(targets, helper_targets), solver=solver, + **kwargs) + else: + return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) # Useful type aliases Model = CombinedBlock + +# A CombinedBlock sub-class specifically for steady state calibration with helper blocks +class SSCalibrationBlock(CombinedBlock): + """An SSCalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering + the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an + SSCalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" + def __init__(self, blocks, helper_blocks, calibration, name=""): + sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) + intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) + + super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) + + self.helper_blocks = helper_blocks + self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) + + self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) + self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig + + def __repr__(self): + return f"" + + def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): + """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" + ss = calibration.copy() + helper_outputs = {} + for block in self.blocks: + if not evaluate_helpers and block in self.helper_blocks: + continue + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) + if evaluate_helpers and block in self.helper_blocks: + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) + ss.update(outputs) + else: + # Don't overwrite entries in ss_values corresponding to what has already + # been solved for in helper_blocks so we can check for consistency after-the-fact + ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) + return ss + + +def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): + """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. + + Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's + inferred) that indicate their aggregate inputs and outputs + + Importantly, because including helper blocks in a blocks without additional measures + can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the + steady_state computation to resolve these cycles. + e.g. Consider Krusell Smith: + Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the helper block + because the "firm" block outputs "r", which the helper block takes as an input. + However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes + "K" as an input. + This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then + "firm" could be removed as a dependency of helper block and the cycle would be resolved. + + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + helper_blocks: `list` + A list of helper blocks + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using helper blocks. Read above docstring for more detail + return_io: `bool` + A boolean indicating whether to return the full set of input and output arguments from `blocks` + """ + if return_io: + # step 1: map outputs to blocks for topological sort + outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, + helper_blocks=helper_blocks, calibration=calibration) + + # step 2: dependency graph for topological sort and input list + dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, + helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep), inargs, outargs + else: + # step 1: map outputs to blocks for topological sort + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) + + # step 2: dependency graph for topological sort and input list + dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep) + + +def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): + """Mirroring construct_output_map functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + + helper_inputs = set().union(*[block.inputs for block in helper_blocks]) + + outmap = dict() + outargs = set() + for num, block in enumerate(blocks): + # Find the relevant set of outputs corresponding to a block + if hasattr(block, "outputs"): + outputs = block.outputs + elif isinstance(block, dict): + outputs = block.keys() + else: + raise ValueError(f'{block} is not recognized as block or does not provide outputs') + + for o in outputs: + # Because some of the outputs of a helper block are, by construction, outputs that also appear in the + # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering + # throwing this ValueError + if o in outmap and block not in helper_blocks: + raise ValueError(f'{o} is output twice') + + # Priority sorting for standard blocks: + # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share + # a given output, such that the dependency graph is constructed on the standard blocks, where possible + if o not in outmap: + outmap[o] = num + if return_output_args and not (o in helper_inputs and o in calibration): + outargs.add(o) + else: + continue + if return_output_args: + return outmap, outargs + else: + return outmap + + +def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, + calibration=None, return_input_args=False, outargs=None): + """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + if outargs is None: + outargs = {} + + dep = {num: set() for num in range(len(blocks))} + inargs = set() + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + # Each potential input to a given block will either be 1) output by another block, + # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into + # the steady-state computation via the `calibration' dict. + # If the block is a helper block, then we want to check the calibration to see if the potential + # input is a pre-specified variable/parameter, and if it is then we will not add the block that + # produces that input as an output as a dependency. + # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic + # dependency, if it were not for this resolution. + if i in outmap and not (i in calibration and block in helper_blocks): + dep[num].add(outmap[i]) + if return_input_args and not (i in outargs): + inargs.add(i) + if return_input_args: + return dep, inargs + else: + return dep + + +def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): + """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module + but augmented to support helper blocks""" + required = set() + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + if i in outmap: + required.add(i) + return required diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index e9ffa14..9dafac7 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -55,7 +55,7 @@ def __init__(self, block: Block, name, unknowns, targets, solver=None, solver_kw def __repr__(self): return f"" - def _steady_state(self, calibration, dissolve=[], unknowns=None, solver=None, ttol=1e-9, ctol=1e-9, verbose=False): + def _steady_state(self, calibration, dissolve=[], unknowns=None, solver="", ttol=1e-9, ctol=1e-9, verbose=False): if self.name in dissolve: solver = "solved" unknowns = {k: v for k, v in calibration.items() if k in self.unknowns} @@ -64,11 +64,11 @@ def _steady_state(self, calibration, dissolve=[], unknowns=None, solver=None, tt # unknown values akin to the steady_state method of Block if unknowns is None: unknowns = self.unknowns - if solver is None: + if not solver: solver = self.solver return self.block.solve_steady_state(calibration, unknowns, self.targets, solver=solver, - ttol=ttol, ctol=ctol, verbose=verbose) + ttol=ttol, ctol=ctol, verbose=verbose) def _impulse_nonlinear(self, ss, inputs, outputs, Js): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 059bb1c..905fcd0 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -9,8 +9,7 @@ from sequence_jacobian.utilities.ordered_set import OrderedSet -from .steady_state.drivers import steady_state as ss -from .steady_state.support import provide_solver_default +from .steady_state.support import provide_solver_default, solve_for_unknowns, compute_target_values from .steady_state.classes import SteadyStateDict, UserProvidedSS from .jacobian.classes import JacobianDict, FactoredJacobianDict from .blocks.support.impulse import ImpulseDict @@ -158,14 +157,34 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], - targets: Union[Array, Dict[str, Union[str, Real]]], - solver: Optional[str] = "", **kwargs) -> SteadyStateDict: + targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: Optional[List] = [], + solver: Optional[str] = "", solver_kwargs: Optional[Dict] = {}, + helper_blocks: Optional[List] = [], helper_targets: Optional[Dict] = {}, + block_kwargs: Optional[Dict] = {}, ttol: Optional[float] = 1e-11, ctol: Optional[float] = 1e-9, + verbose: Optional[bool] = False, check_consistency: Optional[bool] = True, + constrained_method: Optional[str] = "linear_continuation", + constrained_kwargs: Optional[Dict] = {}): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - blocks = self.blocks if hasattr(self, "blocks") else [self] solver = solver if solver else provide_solver_default(unknowns) - return ss(blocks, calibration, unknowns, targets, solver=solver, **kwargs) + + ss = SteadyStateDict(calibration) + def residual(unknown_values, evaluate_helpers=True): + ss.update(misc.smart_zip(unknowns.keys(), unknown_values)) + ss.update(self.steady_state(ss, dissolve=dissolve, helper_targets=helper_targets, + evaluate_helpers=evaluate_helpers, **block_kwargs)) + return compute_target_values(targets, ss) + unknowns_solved = solve_for_unknowns(residual, unknowns, solver, solver_kwargs, tol=ttol, verbose=verbose, + constrained_method=constrained_method, constrained_kwargs=constrained_kwargs) + if helper_blocks and helper_targets and check_consistency: + # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers + unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) + cresid = np.max(abs(residual(unknowns_solved.values(), evaluate_helpers=False))) + if cresid > ctol: + raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" + f" the consistency of the DAG without redirection.") + return ss def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, diff --git a/src/sequence_jacobian/steady_state/drivers.py b/src/sequence_jacobian/steady_state/drivers.py deleted file mode 100644 index f5c0285..0000000 --- a/src/sequence_jacobian/steady_state/drivers.py +++ /dev/null @@ -1,280 +0,0 @@ -"""A general function for computing a model's steady state variables and parameters values""" - -import numpy as np -import scipy.optimize as opt -from copy import deepcopy -from functools import partial - -from .support import compute_target_values, extract_multivariate_initial_values_and_bounds,\ - extract_univariate_initial_values_or_bounds, constrained_multivariate_residual, run_consistency_check,\ - subset_helper_block_unknowns, instantiate_steady_state_mutable_kwargs -from .classes import SteadyStateDict -from ..utilities import solvers, graph, misc -from ..blocks.parent import Parent - - -# Find the steady state solution -def steady_state(blocks, calibration, unknowns, targets, dissolve=None, - sort_blocks=True, helper_blocks=None, helper_targets=None, - consistency_check=True, ttol=2e-12, ctol=1e-9, - block_kwargs=None, verbose=False, solver=None, solver_kwargs=None, - constrained_method="linear_continuation", constrained_kwargs=None): - """ - For a given model (blocks), calibration, unknowns, and targets, solve for the steady state values. - - blocks: `list` - A list of blocks, which include the types: SimpleBlock, HetBlock, SolvedBlock, CombinedBlock - calibration: `dict` - The pre-specified values of variables/parameters provided to the steady state computation - unknowns: `dict` - A dictionary mapping unknown variables to either initial values or bounds to be provided to the numerical solver - targets: `dict` - A dictionary mapping target variables to desired numerical values, other variables solved for along the DAG - dissolve: `list` - A list of blocks, either SolvedBlock or CombinedBlock, where block-level unknowns are removed and subsumed - by the top-level unknowns, effectively removing the "solve" components of the blocks - sort_blocks: `bool` - Whether the blocks need to be topologically sorted (only False when this function is called from within a - Block object, like CombinedBlock, that has already pre-sorted the blocks) - helper_blocks: `list` - A list of blocks that replace some of the equations in the DAG to aid steady state calculation - helper_targets: `list` - A list of target names that are handled by the helper blocks - consistency_check: `bool` - If helper blocks are a portion of the argument blocks, re-run the DAG with the computed steady state values - without the assistance of helper blocks and see if the targets are still hit - ttol: `float` - The tolerance for the targets---how close the user wants the computed target values to equal the desired values - ctol: `float` - The tolerance for the consistency check---how close the user wants the computed target values, without the - use of helper blocks, to equal the desired values - block_kwargs: `dict` - A dict of any kwargs that specify additional settings in order to evaluate block.steady_state for any - potential Block object, e.g. HetBlocks have backward_tol and forward_tol settings that are specific to that - Block sub-class. - verbose: `bool` - Display the content of optional print statements within the solver for more responsive feedback - solver: `string` - The name of the numerical solver that the user would like to user. Can either be a custom solver the user - implemented, or one of the standard root-finding methods in scipy.optim.root_scalar or scipy.optim.root - solver_kwargs: `dict` - The keyword arguments that the user's chosen solver requires to run - constrained_method: `str` - When using solvers that typically only take an initial value, x0, we provide a few options for manipulating - the solver to account for bounds when finding a solution. These methods are described in the - constrained_multivariate_residual function - constrained_kwargs: - The keyword arguments that the user's chosen constrained method requires to run - - return: ss_values: `dict` - A dictionary containing all of the pre-specified values and computed values from the steady state computation - """ - - dissolve, block_kwargs, solver_kwargs, constrained_kwargs =\ - instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs) - - # Initial setup of blocks, targets, and dictionary of steady state values to be returned - blocks_all = blocks - targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - - ss_values = SteadyStateDict(calibration) - unknown_keys = unknowns.keys() - - def residual(unknown_values, bypass_redirection=False): - ss_values.update(misc.smart_zip(unknown_keys, unknown_values)) - - # TODO: Later on optimize to not evaluating blocks in residual that are no longer needed due to helper - # block subsetting - for block in blocks_all: - # TODO: this is duplicate of CombinedBlock inner_dissolve, should offload to that - inner_dissolve = [k for k in dissolve if isinstance(block, Parent) and k in block.descendants] - outputs = block.steady_state(ss_values, hetoutput=True, dissolve=inner_dissolve, - bypass_redirection=bypass_redirection, verbose=verbose, **block_kwargs) - ss_values.update(outputs) - - return compute_target_values(targets, ss_values) - - unknowns_solved = _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, - constrained_method=constrained_method, - constrained_kwargs=constrained_kwargs, - tol=ttol, verbose=verbose) - - # Check that the solution is consistent with what would come out of the DAG without the helper blocks - if consistency_check: - # TODO: Shouldn't the DAG be re-sorted when the helper blocks are stripped out, so it evaluates through in the - # correct order? The sorted DAG here is based on including the helper blocks! So simply omitting them but - # retaining the order that they created may not be entirely correct? - # Add the unknowns not handled by helpers into the DAG to be checked. - unknowns_solved.update({k: ss_values[k] for k in unknowns if k not in unknowns_solved}) - - cresid = np.max(abs(residual(unknowns_solved.values(), bypass_redirection=True))) - if cresid > ctol: - raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" - f" the consistency of the DAG without redirection.") - - # Update to set the solutions for the steady state values of the unknowns - ss_values.update(unknowns_solved) - - return ss_values - - -# def eval_block_ss(block, calibration, toplevel_unknowns=None, dissolve=None, **kwargs): -# """Evaluate the .ss method of a block, given a dictionary of potential arguments""" -# if toplevel_unknowns is None: -# toplevel_unknowns = [] -# if dissolve is None: -# dissolve = [] - -# # Add the block's internal variables as inputs, if the block has an internal attribute -# if isinstance(calibration, dict): -# input_arg_dict = deepcopy(calibration) -# else: -# input_arg_dict = {**calibration.toplevel, **calibration.internal[block.name]} if block.name in calibration.internal else calibration.toplevel - -# # Bypass the behavior for SolvedBlocks to numerically solve for their unknowns and simply evaluate them -# # at the provided set of unknowns if included in dissolve. -# valid_input_kwargs = misc.input_kwarg_list(block._steady_state) -# block_unknowns_in_toplevel_unknowns = set(block.unknowns.keys()).issubset(set(toplevel_unknowns)) if hasattr(block, "unknowns") else False -# input_kwarg_dict = {k: v for k, v in kwargs.items() if k in valid_input_kwargs} -# if block in dissolve and "solver" in valid_input_kwargs: -# input_kwarg_dict["solver"] = "solved" -# input_kwarg_dict["unknowns"] = {k: v for k, v in calibration.items() if k in block.unknowns} -# elif block not in dissolve and block_unknowns_in_toplevel_unknowns: -# raise RuntimeError(f"The block '{block.name}' is not in the kwarg `dissolve` but its unknowns," -# f" {set(block.unknowns.keys())} are a subset of the top-level unknowns," -# f" {set(toplevel_unknowns)}.\n" -# f"If the user provides a set of top-level unknowns that subsume block-level unknowns," -# f" it must be explicitly declared in `dissolve`.") - -# return block.steady_state({k: v for k, v in input_arg_dict.items() if k in block.inputs}, **input_kwarg_dict) - - -def _solve_for_unknowns(residual, unknowns, solver, solver_kwargs, residual_kwargs=None, - constrained_method="linear_continuation", constrained_kwargs=None, - tol=2e-12, verbose=False): - """ - Given a residual function (constructed within steady_state) and a set of bounds or initial values for - the set of unknowns, solve for the root. - TODO: Implemented as a hidden method as of now because this function relies on the structure of steady_state - specifically and will not work with a generic residual function, due to the way it currently expects residual - to call variables not provided as arguments explicitly but that exist in its enclosing scope. - - residual: `function` - A function to be supplied to a numerical solver that takes unknown values as arguments - and returns computed targets. - unknowns: `dict` - Refer to the `steady_state` function docstring for the "unknowns" variable - targets: `dict` - Refer to the `steady_state` function docstring for the "targets" variable - tol: `float` - The absolute convergence tolerance of the computed target to the desired target value in the numerical solver - solver: `str` - Refer to the `steady_state` function docstring for the "solver" variable - solver_kwargs: - Refer to the `steady_state` function docstring for the "solver_kwargs" variable - - return: The root[s] of the residual function as either a scalar (float) or a list of floats - """ - if residual_kwargs is None: - residual_kwargs = {} - - scipy_optimize_uni_solvers = ["bisect", "brentq", "brenth", "ridder", "toms748", "newton", "secant", "halley"] - scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", - "excitingmixing", "krylov", "df-sane"] - - # Wrap kwargs into the residual function - residual_f = partial(residual, **residual_kwargs) - - if solver is None: - raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") - elif solver in scipy_optimize_uni_solvers: - initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) - result = opt.root_scalar(residual_f, method=solver, xtol=tol, - **initial_values_or_bounds, **solver_kwargs) - if not result.converged: - raise ValueError(f"Steady-state solver, {solver}, did not converge.") - unknown_solutions = result.root - elif solver in scipy_optimize_multi_solvers: - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) - # If no bounds were provided - if not bounds: - result = opt.root(residual_f, initial_values, - method=solver, tol=tol, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - result = opt.root(constrained_residual, initial_values, - method=solver, tol=tol, **solver_kwargs) - if not result.success: - raise ValueError(f"Steady-state solver, {solver}, did not converge." - f" The termination status is {result.status}.") - unknown_solutions = list(result.x) - # TODO: Implement a more general interface for custom solvers, so we don't need to add new elifs at this level - # everytime a new custom solver is implemented. - elif solver == "broyden_custom": - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) - # If no bounds were provided - if not bounds: - unknown_solutions, _ = solvers.broyden_solver(residual_f, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, - verbose=verbose, tol=tol, **solver_kwargs) - unknown_solutions = list(unknown_solutions) - elif solver == "newton_custom": - initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) - # If no bounds were provided - if not bounds: - unknown_solutions, _ = solvers.newton_solver(residual_f, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - else: - constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, - method=constrained_method, - **constrained_kwargs) - unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, - tol=tol, verbose=verbose, **solver_kwargs) - unknown_solutions = list(unknown_solutions) - else: - raise RuntimeError(f"steady_state is not yet compatible with {solver}.") - - return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) - - -# def _solve_for_unknowns_w_helper_blocks(residual, unknowns, targets, helper_unknowns, helper_targets, -# solver, solver_kwargs, constrained_method="linear_continuation", -# constrained_kwargs=None, tol=2e-12, verbose=False, fragile=False): -# """Enhance the solver executed in _solve_for_unknowns by handling a subset of unknowns and targets -# with helper blocks, reducing the number of unknowns that need to be numerically solved for.""" -# # Initial evaluation of the DAG at the initial values of the unknowns, including the helper blocks, -# # to populate the `ss_values` dict with the unknown values that: -# # a) are handled by helper blocks and b) are excludable from the main DAG -# # and to populate `helper_outputs` with outputs handled by helpers that ought not be changed -# unknowns_init_vals = [v if not isinstance(v, tuple) else (v[0] + v[1]) / 2 for v in unknowns.values()] -# targets_init_vals = dict(misc.smart_zip(targets.keys(), residual(targets, unknowns.keys(), unknowns_init_vals))) -# -# # Subset out the unknowns and targets that are not excludable from the main DAG loop -# unknowns_non_excl = misc.dict_diff(unknowns, helper_unknowns) -# targets_non_excl = misc.dict_diff(targets, helper_targets) -# -# # If the `targets` that are handled by helpers and excludable from the main DAG evaluate to 0. at the set of -# # `unknowns` initial values and the initial `calibration`, then those `targets` have been hit analytically and -# # we can omit them and their corresponding `unknowns` in the main DAG. -# if np.all(np.isclose([targets_init_vals[t] for t in helper_targets.keys()], 0.)): -# unknown_solutions = _solve_for_unknowns(residual, unknowns_non_excl, targets_non_excl, -# solver, solver_kwargs, -# constrained_method=constrained_method, -# constrained_kwargs=constrained_kwargs, -# tol=tol, verbose=verbose, fragile=fragile) -# # If targets handled by helpers and excludable from the main DAG are not satisfied then -# # it is assumed that helper blocks merely aid in providing more accurate guesses for the DAG solution, -# # and they remain a part of the main DAG when solving. -# else: -# unknown_solutions = _solve_for_unknowns(residual, unknowns, targets, solver, solver_kwargs, -# constrained_method=constrained_method, -# constrained_kwargs=constrained_kwargs, -# tol=tol, verbose=verbose, fragile=fragile) -# return unknown_solutions diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py index f45471d..c27679c 100644 --- a/src/sequence_jacobian/steady_state/support.py +++ b/src/sequence_jacobian/steady_state/support.py @@ -1,8 +1,12 @@ """Various lower-level functions to support the computation of steady states""" import warnings -from numbers import Real import numpy as np +import scipy.optimize as opt +from numbers import Real +from functools import partial + +from ..utilities import misc, solvers def instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs): @@ -133,6 +137,102 @@ def compare_steady_states(ss_ref, ss_comp, tol=1e-8, name_map=None, internal=Tru return valid +def solve_for_unknowns(residual, unknowns, solver, solver_kwargs, residual_kwargs=None, + constrained_method="linear_continuation", constrained_kwargs=None, + tol=2e-12, verbose=False): + """Given a residual function (constructed within steady_state) and a set of bounds or initial values for + the set of unknowns, solve for the root. + + residual: `function` + A function to be supplied to a numerical solver that takes unknown values as arguments + and returns computed targets. + unknowns: `dict` + Refer to the `steady_state` function docstring for the "unknowns" variable + targets: `dict` + Refer to the `steady_state` function docstring for the "targets" variable + tol: `float` + The absolute convergence tolerance of the computed target to the desired target value in the numerical solver + solver: `str` + Refer to the `steady_state` function docstring for the "solver" variable + solver_kwargs: + Refer to the `steady_state` function docstring for the "solver_kwargs" variable + + return: The root[s] of the residual function as either a scalar (float) or a list of floats + """ + if residual_kwargs is None: + residual_kwargs = {} + + scipy_optimize_uni_solvers = ["bisect", "brentq", "brenth", "ridder", "toms748", "newton", "secant", "halley"] + scipy_optimize_multi_solvers = ["hybr", "lm", "broyden1", "broyden2", "anderson", "linearmixing", "diagbroyden", + "excitingmixing", "krylov", "df-sane"] + + # Wrap kwargs into the residual function + residual_f = partial(residual, **residual_kwargs) + + if solver is None: + raise RuntimeError("Must provide a numerical solver from the following set: brentq, broyden, solved") + elif solver in scipy_optimize_uni_solvers: + initial_values_or_bounds = extract_univariate_initial_values_or_bounds(unknowns) + result = opt.root_scalar(residual_f, method=solver, xtol=tol, + **initial_values_or_bounds, **solver_kwargs) + if not result.converged: + raise ValueError(f"Steady-state solver, {solver}, did not converge.") + unknown_solutions = result.root + elif solver in scipy_optimize_multi_solvers: + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + # If no bounds were provided + if not bounds: + result = opt.root(residual_f, initial_values, + method=solver, tol=tol, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + result = opt.root(constrained_residual, initial_values, + method=solver, tol=tol, **solver_kwargs) + if not result.success: + raise ValueError(f"Steady-state solver, {solver}, did not converge." + f" The termination status is {result.status}.") + unknown_solutions = list(result.x) + # TODO: Implement a more general interface for custom solvers, so we don't need to add new elifs at this level + # everytime a new custom solver is implemented. + elif solver == "broyden_custom": + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + # If no bounds were provided + if not bounds: + unknown_solutions, _ = solvers.broyden_solver(residual_f, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + unknown_solutions, _ = solvers.broyden_solver(constrained_residual, initial_values, + verbose=verbose, tol=tol, **solver_kwargs) + unknown_solutions = list(unknown_solutions) + elif solver == "newton_custom": + initial_values, bounds = extract_multivariate_initial_values_and_bounds(unknowns) + # If no bounds were provided + if not bounds: + unknown_solutions, _ = solvers.newton_solver(residual_f, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + else: + constrained_residual = constrained_multivariate_residual(residual_f, bounds, verbose=verbose, + method=constrained_method, + **constrained_kwargs) + unknown_solutions, _ = solvers.newton_solver(constrained_residual, initial_values, + tol=tol, verbose=verbose, **solver_kwargs) + unknown_solutions = list(unknown_solutions) + elif solver == "solved": + # If the model either doesn't require a numerical solution or is being evaluated at a candidate solution + # simply call residual_f once to populate the `ss_values` dict + residual_f(unknowns.values()) + unknown_solutions = unknowns.values() + else: + raise RuntimeError(f"steady_state is not yet compatible with {solver}.") + + return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) + + def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): """Find the set of unknowns that the `helper_blocks` solve for""" unknowns_handled_by_helpers = {} From 897e064ffff5aed09233314481f182aa003db608 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 2 Sep 2021 14:28:58 -0400 Subject: [PATCH 246/288] Refactored JacobianDictBlock; made outputs= optional in impulse_linear. --- .../blocks/auxiliary_blocks/jacobiandict_block.py | 12 ++++-------- src/sequence_jacobian/primitives.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index 95c8ab9..735105b 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -1,10 +1,8 @@ """A simple wrapper for JacobianDicts to be embedded in DAGs""" -from numbers import Real -from typing import Dict, Union, List - from ...primitives import Block, Array from ...jacobian.classes import JacobianDict +from ..support.impulse import ImpulseDict class JacobianDictBlock(JacobianDict, Block): @@ -15,12 +13,10 @@ def __init__(self, nesteddict, outputs=None, inputs=None, name=None): def __repr__(self): return f"" - def impulse_linear(self, ss: Dict[str, Union[Real, Array]], - exogenous: Dict[str, Array], **kwargs) -> Dict[str, Array]: - return self.jacobian(list(exogenous.keys())).apply(exogenous) + def _impulse_linear(self, ss, inputs, outputs, Js): + return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) - def _jacobian(self, ss, inputs, outputs, T) -> JacobianDict: - # TODO: T should be an attribute of JacobianDict + def _jacobian(self, ss, inputs, outputs, T): if not inputs <= self.inputs: raise KeyError(f'Asking JacobianDictBlock for {inputs - self.inputs}, which are among its inputs {self.inputs}') if not outputs <= self.outputs: diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index b9cf19a..1305378 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -202,7 +202,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ return results | U def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], - inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]], + inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous From 10b0f9ce868d5b8c88293fbd536ca3eb0939ab7f Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 7 Sep 2021 12:54:34 -0500 Subject: [PATCH 247/288] added het_support and tests, will be used for full refactoring of het_block forward iterations --- .../blocks/support/het_compiled.py | 104 ++++++++ .../blocks/support/het_support.py | 238 ++++++++++++++++++ tests/base/test_het_support.py | 175 +++++++++++++ 3 files changed, 517 insertions(+) create mode 100644 src/sequence_jacobian/blocks/support/het_compiled.py create mode 100644 src/sequence_jacobian/blocks/support/het_support.py create mode 100644 tests/base/test_het_support.py diff --git a/src/sequence_jacobian/blocks/support/het_compiled.py b/src/sequence_jacobian/blocks/support/het_compiled.py new file mode 100644 index 0000000..ddb9efa --- /dev/null +++ b/src/sequence_jacobian/blocks/support/het_compiled.py @@ -0,0 +1,104 @@ +import numpy as np +from numba import njit + +@njit +def forward_policy_1d(D, x_i, x_pi): + nZ, nX = D.shape + Dnew = np.zeros_like(D) + for iz in range(nZ): + for ix in range(nX): + i = x_i[iz, ix] + pi = x_pi[iz, ix] + d = D[iz, ix] + + Dnew[iz, i] += d * pi + Dnew[iz, i+1] += d * (1 - pi) + + return Dnew + + +@njit +def expectations_policy_1d(X, x_i, x_pi): + nZ, nX = X.shape + Xnew = np.zeros_like(X) + for iz in range(nZ): + for ix in range(nX): + i = x_i[iz, ix] + pi = x_pi[iz, ix] + Xnew[iz, ix] = pi * X[iz, i] + (1-pi) * X[iz, i+1] + return Xnew + + +@njit +def forward_policy_shock_1d(Dss, x_i_ss, x_pi_shock): + """forward_step_1d linearized wrt x_pi""" + nZ, nX = Dss.shape + Dshock = np.zeros_like(Dss) + for iz in range(nZ): + for ix in range(nX): + i = x_i_ss[iz, ix] + dshock = x_pi_shock[iz, ix] * Dss[iz, ix] + Dshock[iz, i] += dshock + Dshock[iz, i + 1] -= dshock + + return Dshock + + +@njit +def forward_policy_2d(D, x_i, y_i, x_pi, y_pi): + nZ, nX, nY = D.shape + Dnew = np.zeros_like(D) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i[iz, ix, iy] + iyp = y_i[iz, ix, iy] + beta = x_pi[iz, ix, iy] + alpha = y_pi[iz, ix, iy] + + Dnew[iz, ixp, iyp] += alpha * beta * D[iz, ix, iy] + Dnew[iz, ixp+1, iyp] += alpha * (1 - beta) * D[iz, ix, iy] + Dnew[iz, ixp, iyp+1] += (1 - alpha) * beta * D[iz, ix, iy] + Dnew[iz, ixp+1, iyp+1] += (1 - alpha) * (1 - beta) * D[iz, ix, iy] + return Dnew + + +@njit +def expectations_policy_2d(X, x_i, y_i, x_pi, y_pi): + nZ, nX, nY = X.shape + Xnew = np.empty_like(X) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i[iz, ix, iy] + iyp = y_i[iz, ix, iy] + alpha = x_pi[iz, ix, iy] + beta = y_pi[iz, ix, iy] + + Xnew[iz, ix, iy] = (alpha * beta * X[iz, ixp, iyp] + alpha * (1-beta) * X[iz, ixp, iyp+1] + + (1-alpha) * beta * X[iz, ixp+1, iyp] + + (1-alpha) * (1-beta) * X[iz, ixp+1, iyp+1]) + return Xnew + + +@njit +def forward_policy_shock_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): + """Endogenous update part of forward_step_shock_2d""" + nZ, nX, nY = Dss.shape + Dshock = np.zeros_like(Dss) + for iz in range(nZ): + for ix in range(nX): + for iy in range(nY): + ixp = x_i_ss[iz, ix, iy] + iyp = y_i_ss[iz, ix, iy] + alpha = x_pi_ss[iz, ix, iy] + beta = y_pi_ss[iz, ix, iy] + + dalpha = x_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] + dbeta = y_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] + + Dshock[iz, ixp, iyp] += dalpha * beta + alpha * dbeta + Dshock[iz, ixp+1, iyp] += dbeta * (1-alpha) - beta * dalpha + Dshock[iz, ixp, iyp+1] += dalpha * (1-beta) - alpha * dbeta + Dshock[iz, ixp+1, iyp+1] -= dalpha * (1-beta) + dbeta * (1-alpha) + return Dshock diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py new file mode 100644 index 0000000..1ff748d --- /dev/null +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -0,0 +1,238 @@ +import numpy as np +from . import het_compiled +from ...utilities.discretize import stationary as general_stationary +from ...utilities.interpolate import interpolate_coord_robust +from typing import Optional, Sequence, Any, List, Tuple, Union + +class Transition: + """Abstract class for PolicyLottery or ManyMarkov, i.e. some part of state-space transition""" + def forward(self, D): + pass + + def expectations(self, X): + pass + + def shockable(self, Dss): + # return ShockableTransition + pass + + +class ShockableTransition(Transition): + """Abstract class extending Transition, allowing us to find effect of shock to transition rule + on one-period-ahead distribution. This functionality isn't included in the regular Transition + because it requires knowledge of the incoming ("steady-state") distribution and also sometimes + some precomputation. + + One crucial thing here is the order of shock arguments in shocks. Also, is None is the default + argument for a shock, we allow that shock to be None. We always allow shocks in lists to be None.""" + + def forward_shock(self, shocks): + pass + + +def lottery_1d(a, a_grid): + return PolicyLottery1D(*interpolate_coord_robust(a_grid, a), a_grid) + + +class PolicyLottery1D(Transition): + # TODO: always operates on final dimension, highly non-generic in that sense + def __init__(self, i, pi, grid): + # flatten non-policy dimensions into one because that's what methods accept + self.i = i.reshape((-1,) + grid.shape) + self.flatshape = self.i.shape + + self.pi = pi.reshape(self.flatshape) + + # but store original shape so we can convert all outputs to it + self.shape = i.shape + self.grid = grid + + # also store shape of the endogenous grid itself + self.endog_shape = self.shape[-1:] + + def forward(self, D): + return het_compiled.forward_policy_1d(D.reshape(self.flatshape), self.i, self.pi).reshape(self.shape) + + def expectations(self, X): + return het_compiled.expectations_policy_1d(X.reshape(self.flatshape), self.i, self.pi).reshape(self.shape) + + def shockable(self, Dss): + return ShockablePolicyLottery1D(self.i.reshape(self.shape), self.pi.reshape(self.shape), + self.grid, Dss) + + +class ShockablePolicyLottery1D(PolicyLottery1D, ShockableTransition): + def __init__(self, i, pi, grid, Dss): + super().__init__(i, pi, grid) + self.Dss = Dss.reshape(self.flatshape) + self.space = grid[self.i+1] - grid[self.i] + + def forward_shock(self, da, dgrid=None): + # TODO: think about da being None too for more general applications + pi_shock = - da.reshape(self.flatshape) / self.space + + if dgrid is not None: + # see "linearizing_interpolation" note + dgrid = np.broadcast_to(dgrid, self.shape) + pi_shock += self.expectations(dgrid).reshape(self.flatshape) / self.space + + return het_compiled.forward_policy_shock_1d(self.Dss, self.i, pi_shock).reshape(self.shape) + + +def lottery_2d(a, b, a_grid, b_grid): + return PolicyLottery2D(*interpolate_coord_robust(a_grid, a), + *interpolate_coord_robust(b_grid, b), a_grid, b_grid) + + +class PolicyLottery2D(Transition): + def __init__(self, i1, pi1, i2, pi2, grid1, grid2): + # flatten non-policy dimensions into one because that's what methods accept + self.i1 = i1.reshape((-1,) + grid1.shape + grid2.shape) + self.flatshape = self.i1.shape + + self.i2 = i2.reshape(self.flatshape) + self.pi1 = pi1.reshape(self.flatshape) + self.pi2 = pi2.reshape(self.flatshape) + + # but store original shape so we can convert all outputs to it + self.shape = i1.shape + self.grid1 = grid1 + self.grid2 = grid2 + + # also store shape of the endogenous grid itself + self.endog_shape = self.shape[-2:] + + def forward(self, D): + return het_compiled.forward_policy_2d(D.reshape(self.flatshape), self.i1, self.i2, + self.pi1, self.pi2).reshape(self.shape) + + def expectations(self, X): + return het_compiled.expectations_policy_2d(X.reshape(self.flatshape), self.i1, self.i2, + self.pi1, self.pi2).reshape(self.shape) + + def shockable(self, Dss): + return ShockablePolicyLottery2D(self.i1.reshape(self.shape), self.pi1.reshape(self.shape), + self.i2.reshape(self.shape), self.pi2.reshape(self.shape), + self.grid1, self.grid2, Dss) + + +class ShockablePolicyLottery2D(PolicyLottery2D, ShockableTransition): + def __init__(self, i1, pi1, i2, pi2, grid1, grid2, Dss): + super().__init__(i1, pi1, i2, pi2, grid1, grid2) + self.Dss = Dss.reshape(self.flatshape) + self.space1 = grid1[self.i1+1] - grid1[self.i1] + self.space2 = grid2[self.i2+1] - grid2[self.i2] + + def forward_shock(self, da1, da2, dgrid1=None, dgrid2=None): + pi_shock1 = -da1.reshape(self.flatshape) / self.space1 + pi_shock2 = -da2.reshape(self.flatshape) / self.space2 + + if dgrid1 is not None: + dgrid1 = np.broadcast_to(dgrid1[:, np.newaxis], self.shape) + pi_shock1 += self.expectations(dgrid1).reshape(self.flatshape) / self.space1 + + if dgrid2 is not None: + dgrid2 = np.broadcast_to(dgrid2, self.shape) + pi_shock2 += self.expectations(dgrid2).reshape(self.flatshape) / self.space2 + + return het_compiled.forward_policy_shock_2d(self.Dss, self.i1, self.i2, self.pi1, self.pi2, + pi_shock1, pi_shock2).reshape(self.shape) + + +def multiply_ith_dimension(Pi, i, X): + """If Pi is a square matrix, multiply Pi times the ith dimension of X and return""" + X = X.swapaxes(0, i) + shape = X.shape + X = X.reshape((X.shape[0], -1)) + + # iterate forward using Pi + X = Pi @ X + + # reverse steps + X = X.reshape(shape) + return X.swapaxes(0, i) + + +class Markov(Transition): + def __init__(self, Pi, i): + self.Pi = Pi + self.Pi_T = self.Pi.T + if isinstance(self.Pi_T, np.ndarray): + # optimization: copy to get right order in memory + self.Pi_T = self.Pi_T.copy() + self.i = i + + def forward(self, D): + return multiply_ith_dimension(self.Pi_T, self.i, D) + + def expectations(self, X): + return multiply_ith_dimension(self.Pi, self.i, X) + + def shockable(self, Dss): + return ShockableMarkov(self.Pi, self.i, Dss) + + def stationary(self, tol=1E-11, maxit=10_000): + pi_seed = getattr(self.Pi, 'pi_seed', None) + return general_stationary(self.Pi, pi_seed, tol, maxit) + + +class ShockableMarkov(Markov, ShockableTransition): + def __init__(self, Pi, i, Dss): + super().__init__(Pi, i) + self.Dss = Dss + + def forward_shock(self, dPi): + return multiply_ith_dimension(dPi.T, self.i, self.Dss) + + +class CombinedTransition(Transition): + def __init__(self, stages: Sequence[Transition]): + self.stages = stages + + def forward(self, D): + for stage in self.stages: + D = stage.forward(D) + return D + + def expectations(self, X): + for stage in reversed(self.stages): + X = stage.expectations(X) + return X + + def shockable(self, Dss): + shockable_stages = [] + for stage in self.stages: + shockable_stages.append(stage.shockable(Dss)) + Dss = stage.forward(Dss) + + return ShockableCombinedTransition(shockable_stages) + +Shock = Any +ListTupleShocks = Union[List[Shock], Tuple[Shock]] + +class ShockableCombinedTransition(CombinedTransition, ShockableTransition): + def __init__(self, stages: Sequence[ShockableTransition]): + self.stages = stages + + def forward_shock(self, shocks: Sequence[Optional[Union[Shock, ListTupleShocks]]]): + # each entry of shocks is either a sequence (list or tuple) + dD = None + + for stage, shock in zip(self.stages, shocks): + if shock is not None: + if isinstance(shock, tuple) or isinstance(shock, list): + dD_shock = stage.forward_shock(*shock) + else: + dD_shock = stage.forward_shock(shock) + else: + dD_shock = None + + if dD is not None: + dD = stage.forward(dD) + + if shock is not None: + dD += dD_shock + else: + dD = dD_shock + + return dD diff --git a/tests/base/test_het_support.py b/tests/base/test_het_support.py new file mode 100644 index 0000000..72d8fdc --- /dev/null +++ b/tests/base/test_het_support.py @@ -0,0 +1,175 @@ +import numpy as np +from sequence_jacobian.blocks.support.het_support import (Transition, + PolicyLottery1D, PolicyLottery2D, Markov, CombinedTransition, + lottery_1d, lottery_2d) + +def test_combined_markov(): + shape = (5, 6, 7) + np.random.seed(12345) + + for _ in range(10): + D = np.random.rand(*shape) + Pis = [np.random.rand(s, s) for s in shape[:2]] + markovs = [Markov(Pi, i) for i, Pi in enumerate(Pis)] + combined = CombinedTransition(markovs) + + Dout = combined.expectations(D) + Dout_forward = combined.forward(D) + + D_kron = D.reshape((-1, D.shape[2])) + Pi_kron = np.kron(Pis[0], Pis[1]) + Dout2 = (Pi_kron @ D_kron).reshape(Dout.shape) + Dout2_forward = (Pi_kron.T @ D_kron).reshape(Dout.shape) + + assert np.allclose(Dout, Dout2) + assert np.allclose(Dout_forward, Dout2_forward) + + +def test_many_markov_shock(): + shape = (5, 6, 7) + np.random.seed(12345) + + for _ in range(10): + D = np.random.rand(*shape) + Pis = [np.random.rand(s, s) for s in shape[:2]] + dPis = [np.random.rand(s, s) for s in shape[:2]] + + h = 1E-4 + Dout_up = CombinedTransition([Markov(Pi + h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))]).forward(D) + Dout_dn = CombinedTransition([Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))]).forward(D) + Dder = (Dout_up - Dout_dn) / (2*h) + + Dder2 = CombinedTransition([Markov(Pi, i) for i, Pi in enumerate(Pis)]).shockable(D).forward_shock(dPis) + + assert np.allclose(Dder, Dder2) + + +def test_policy_and_grid_shock(): + shape = (3, 4, 30) + grid = np.geomspace(0.5, 10, shape[-1]) + np.random.seed(98765) + + a = (np.full(shape[0], 0.01)[:, np.newaxis, np.newaxis] + + np.linspace(0, 1, shape[1])[:, np.newaxis] + + 0.001*grid**2 + 0.9*grid + 0.5) + + for _ in range(10): + D = np.random.rand(*shape) + + da = np.random.rand(*shape) + dgrid = np.random.rand(len(grid)) + h = 1E-5 + Dout_up = lottery_1d(a + h*da, grid + h*dgrid).forward(D) + Dout_dn = lottery_1d(a - h*da, grid - h*dgrid).forward(D) + Dder = (Dout_up - Dout_dn) / (2*h) + + Dder2 = lottery_1d(a, grid).shockable(D).forward_shock(da, dgrid) + + assert np.allclose(Dder, Dder2, atol=1E-4) + + +def test_law_of_motion_shock(): + # shock everything in the law of motion, and see if it works! + shape = (3, 4, 30) + grid = np.geomspace(0.5, 10, shape[-1]) + np.random.seed(98765) + + a = (np.full(shape[0], 0.01)[:, np.newaxis, np.newaxis] + + np.linspace(0, 1, shape[1])[:, np.newaxis] + + 0.001*grid**2 + 0.9*grid + 0.5) + + for _ in range(10): + D = np.random.rand(*shape) + Pis = [np.random.rand(s, s) for s in shape[:2]] + + da = np.random.rand(*shape) + dgrid = np.random.rand(len(grid)) + dPis = [np.random.rand(s, s) for s in shape[:2]] + + h = 1E-5 + policy_up = lottery_1d(a + h*da, grid + h*dgrid) + policy_dn = lottery_1d(a - h*da, grid - h*dgrid) + markovs_up = [Markov(Pi + h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] + markovs_dn =[Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] + Dout_up = CombinedTransition([policy_up, *markovs_up]).forward(D) + Dout_dn = CombinedTransition([policy_dn, *markovs_dn]).forward(D) + Dder = (Dout_up - Dout_dn) / (2*h) + + markovs = [Markov(Pi, i) for i, Pi, in enumerate(Pis)] + Dder2 = CombinedTransition([lottery_1d(a, grid), *markovs]).shockable(D).forward_shock([(da, dgrid), *dPis]) + + assert np.allclose(Dder, Dder2, atol=1E-4) + + +def test_2d_policy_and_grid_shock(): + shape = (3, 4, 20, 30) + a_grid = np.geomspace(0.5, 10, shape[-2]) + b_grid = np.geomspace(0.2, 8, shape[-1]) + np.random.seed(98765) + + a = (0.001*a_grid**2 + 0.9*a_grid + 0.5)[:, np.newaxis] + b = (-0.001*b_grid**2 + 0.9*b_grid + 0.5) + + a = np.broadcast_to(a, shape) + b = np.broadcast_to(b, shape) + + for _ in range(10): + D = np.random.rand(*shape) + Pis = [np.random.rand(s, s) for s in shape[:2]] + + da = np.random.rand(*shape) + db = np.random.rand(*shape) + da_grid = np.random.rand(len(a_grid)) + db_grid = np.random.rand(len(b_grid)) + dPis = [np.random.rand(s, s) for s in shape[:2]] + + h = 1E-5 + + policy_up = lottery_2d(a + h*da, b + h*db, a_grid + h*da_grid, b_grid + h*db_grid) + policy_dn = lottery_2d(a - h*da, b - h*db, a_grid - h*da_grid, b_grid - h*db_grid) + markovs_up = [Markov(Pi + h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] + markovs_dn = [Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] + Dout_up = CombinedTransition([policy_up, *markovs_up]).forward(D) + Dout_dn = CombinedTransition([policy_dn, *markovs_dn]).forward(D) + Dder = (Dout_up - Dout_dn) / (2*h) + + policy = lottery_2d(a, b, a_grid, b_grid) + + markovs = [Markov(Pi, i) for i, Pi, in enumerate(Pis)] + Dder2 = CombinedTransition([policy, *markovs]).shockable(D).forward_shock([[da, db, da_grid, db_grid], *dPis]) + + assert np.allclose(Dder, Dder2, atol=1E-4) + + +def test_forward_expectations_symmetry(): + # given a random law of motion, should be identical to iterate forward on distribution, + # then aggregate, or take expectations backward on outcome, then aggregate + shape = (3, 4, 30) + grid = np.geomspace(0.5, 10, shape[-1]) + np.random.seed(1423) + + a = (np.full(shape[0], 0.01)[:, np.newaxis, np.newaxis] + + np.linspace(0, 1, shape[1])[:, np.newaxis] + + 0.001*grid**2 + 0.9*grid + 0.5) + + for _ in range(10): + D = np.random.rand(*shape) + X = np.random.rand(*shape) + Pis = [np.random.rand(s, s) for s in shape[:2]] + + markovs = [Markov(Pi, i) for i, Pi, in enumerate(Pis)] + lom = CombinedTransition([lottery_1d(a, grid), *markovs]) + + Dforward = D + for _ in range(30): + Dforward = lom.forward(Dforward) + outcome = np.vdot(Dforward, X) + + Xbackward = X + for _ in range(30): + Xbackward = lom.expectations(Xbackward) + outcome2 = np.vdot(D, Xbackward) + + assert np.isclose(outcome, outcome2) + + From b827a6a39192a381bbc510fbaef3cd911b283c04 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 7 Sep 2021 15:00:21 -0500 Subject: [PATCH 248/288] jacobian and ss modified to work with new Transition objects, curlyY/curlyD/curlyE are now beginning-of-period concepts in anticipation of Pi shocks --- src/sequence_jacobian/blocks/het_block.py | 164 +++++++----------- .../blocks/support/het_support.py | 7 +- .../utilities/ordered_set.py | 3 + tests/base/test_estimation.py | 3 + 4 files changed, 71 insertions(+), 106 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 5eaaef4..7125ba4 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -11,6 +11,7 @@ from .support.bijection import Bijection from ..utilities.function import ExtendedFunction, ExtendedParallelFunction from ..utilities.ordered_set import OrderedSet +from .support.het_support import ShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition def het(exogenous, policy, backward, backward_init=None): @@ -71,7 +72,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] self.inputs.remove(exogenous + '_p') self.inputs.add(exogenous) - self.internal = OrderedSet(['D', self.exogenous]) | self.back_step_fun.outputs + self.internal = OrderedSet(['D', 'Dbeg', self.exogenous]) | self.back_step_fun.outputs # store "original" copies of these for use whenever we process new hetinputs/hetoutputs self.original_inputs = self.inputs @@ -172,7 +173,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, ss : dict, contains - ss inputs of self.back_step_fun and (if present) self.hetinput - ss outputs of self.back_step_fun - - ss distribution 'D' + - ss distribution 'D' (and end-of-period distribution 'Dbeg') - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars """ @@ -191,8 +192,8 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, ss.update(sspol) # run forward iteration - D = self.dist_ss(Pi, sspol, grid, forward_tol, forward_maxit, D_seed, pi_seed) - ss.update({"D": D}) + Dbeg, D = self.dist_ss(ss, forward_tol, forward_maxit, D_seed, pi_seed) + ss.update({'Dbeg': Dbeg, "D": D}) # run hetoutput if it's there if self.hetoutputs is not None: @@ -317,22 +318,22 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): outputs = self.M_outputs.inv @ outputs # horrible # step 0: preliminary processing of steady state - Pi, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, sspol_i, sspol_pi, sspol_space = self.jac_prelim(ss, h) + differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h) + law_of_motion = self.make_law_of_motion(ss).shockable(ss['Dbeg']) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in inputs: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, differentiable_back_step_fun, - ss['D'], Pi.T.copy(), - sspol_i, sspol_pi, sspol_space, T, - differentiable_hetinputs, differentiable_hetoutputs) + curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, T, differentiable_back_step_fun, + differentiable_hetinputs, differentiable_hetoutputs, + law_of_motion, ss['D']) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o curlyPs = {} for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss[o], Pi, sspol_i, sspol_pi, T-1) + curlyPs[o] = self.forward_iteration_fakenews(ss[o], T-1, law_of_motion) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair @@ -452,63 +453,37 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): ssin[k] = original_ssin[k] return {**ssin, **sspol} - def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): - """Find steady-state distribution through forward iteration until convergence. - - Parameters - ---------- - Pi : array - steady-state Markov matrix for exogenous variable - sspol : dict - steady-state policies on grid for all policy variables in self.policy - grid : dict - grids for all policy variables in self.policy - tol : [optional] float - absolute tolerance for max diff between consecutive iterations for distribution - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - D_seed : [optional] array - initial seed for overall distribution - pi_seed : [optional] array - initial seed for stationary dist of Pi, if no D_seed - - Returns - ---------- - D : array - steady-state distribution - """ - + def dist_ss(self, ss, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): + law_of_motion = self.make_law_of_motion(ss) + # first obtain initial distribution D if D_seed is None: + # TODO: remember we need to change this once we have more than one exogenous # compute stationary distribution for exogenous variable - pi = utils.discretize.stationary(Pi, pi_seed) + pi = law_of_motion.stages[0].stationary(pi_seed) # now initialize full distribution with this, assuming uniform distribution on endogenous vars - endogenous_dims = [grid[k].shape[0] for k in self.policy] + endogenous_dims = [ss[k+'_grid'].shape[0] for k in self.policy] D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) else: D = D_seed - # obtain interpolated policy rule for each dimension of endogenous policy - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - # use robust binary search-based method that only requires grids, not policies, to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], sspol[pol]) - # iterate until convergence by tol, or maxit - Pi_T = Pi.T.copy() for it in range(maxit): - Dnew = self.forward_step(D, Pi_T, sspol_i, sspol_pi) + Dbeg_new = law_of_motion[1].forward(D) + D_new = law_of_motion[0].forward(Dbeg_new) # only check convergence every 10 iterations for efficiency - if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): + if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, D_new, tol): break - D = Dnew + Dbeg = Dbeg_new + D = D_new else: raise ValueError(f'No convergence after {maxit} forward iterations!') - return D + # "D" is after the exogenous shock, Dbeg is before it + D = law_of_motion.stages[0].forward(Dbeg) + return Dbeg, D '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs @@ -518,15 +493,16 @@ def dist_ss(self, Pi, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None, pi_see ''' def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, - differentiable_hetoutput, Dss, Pi_T, sspol_i, sspol_pi, sspol_space): + differentiable_hetoutput, law_of_motion: ShockableTransition, Dss): + # shock perturbs outputs shocked_outputs = differentiable_back_step_fun.diff(din_dict) curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} # which affects the distribution tomorrow - pol_pi_shock = {k: -shocked_outputs[k] / sspol_space[k] for k in self.policy} - - curlyD = self.forward_step_shock(Dss, Pi_T, sspol_i, sspol_pi, pol_pi_shock) + policy_shock = [shocked_outputs[k] for k in self.policy] + curlyD = law_of_motion.forward_shock([None, policy_shock]) + #curlyD = law_of_motion[0].forward(curlyDbeg) # and the aggregate outcomes today if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): @@ -535,8 +511,8 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step return curlyV, curlyD, curlyY - def backward_iteration_fakenews(self, input_shocked, output_list, differentiable_back_step_fun, Dss, Pi_T, - sspol_i, sspol_pi, sspol_space, T, differentiable_hetinput, differentiable_hetoutput): + def backward_iteration_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, + differentiable_hetinput, differentiable_hetoutput, law_of_motion: ShockableTransition, Dss): """Iterate policy steps backward T times for a single shock.""" if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: # if input_shocked is an input to hetinput, take numerical diff to get response @@ -547,7 +523,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, differentiable # contemporaneous response to unit scalar shock curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, differentiable_hetoutput, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space) + law_of_motion, Dss) # infer dimensions from this and initialize empty arrays curlyDs = np.empty((T,) + curlyD.shape) @@ -562,34 +538,30 @@ def backward_iteration_fakenews(self, input_shocked, output_list, differentiable for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, output_list, differentiable_back_step_fun, differentiable_hetoutput, - Dss, Pi_T, sspol_i, sspol_pi, sspol_space) + law_of_motion, Dss) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] return curlyYs, curlyDs - def forward_iteration_fakenews(self, o_ss, Pi, pol_i_ss, pol_pi_ss, T): - """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. + def forward_iteration_fakenews(self, o_ss, T, law_of_motion: ShockableTransition): + """Iterate transpose forward T steps to get full set of curlyEs for a given outcome.""" + curlyEs = np.empty((T,) + o_ss.shape) - Note we depart from definition in paper by applying the demeaning operator in addition to Lambda - at each step. This does not affect products with curlyD (which are the only way curlyPs enter - Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits - since curlyPs now go to zero for high t (used in paper in proof of Proposition 1). - """ - curlyPs = np.empty((T,) + o_ss.shape) - curlyPs[0, ...] = utils.misc.demean(o_ss) + # initialize with beginning-of-period expectation of policy + curlyEs[0, ...] = utils.misc.demean(law_of_motion[0].expectations(o_ss)) for t in range(1, T): - curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], - Pi, pol_i_ss, pol_pi_ss)) - return curlyPs + # we demean so that curlyEs converge to zero (better numerically), in theory no effect + curlyEs[t, ...] = utils.misc.demean(law_of_motion.expectations(curlyEs[t-1, ...])) + return curlyEs @staticmethod - def build_F(curlyYs, curlyDs, curlyPs): + def build_F(curlyYs, curlyDs, curlyEs): T = curlyDs.shape[0] - Tpost = curlyPs.shape[0] - T + 2 + Tpost = curlyEs.shape[0] - T + 2 F = np.empty((Tpost + T - 1, T)) F[0, :] = curlyYs - F[1:, :] = curlyPs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T + F[1:, :] = curlyEs.reshape((Tpost + T - 2, -1)) @ curlyDs.reshape((T, -1)).T return F @staticmethod @@ -601,47 +573,18 @@ def J_from_F(F): '''Part 5: helpers for .jac and .ajac: preliminary processing''' - def jac_prelim(self, ss, h): - """Helper that does preliminary processing of steady state for fake news algorithm. - - Parameters - ---------- - ss : dict, all steady-state info, intended to be from .ss() - - Returns - ---------- - ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step - Pi : array (S*S), Markov matrix for exogenous state - ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same - as steady-state numerically since SS convergence was to some tolerance threshold) - ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) - sspol_i : dict, indices on lower bracketing gridpoint for all in self.policy - sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy - sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy - """ - Pi = ss[self.exogenous] - grid = {k: ss[k+'_grid'] for k in self.policy} + def jac_backward_prelim(self, ss, h): differentiable_back_step_fun = self.back_step_fun.differentiable(self.make_inputs(ss), h=h) differentiable_hetinputs = None if self.hetinputs is not None: - # ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} differentiable_hetinputs = self.hetinputs.differentiable(ss) differentiable_hetoutputs = None if self.hetoutputs is not None: differentiable_hetoutputs = self.hetoutputs.differentiable(ss) - # preliminary b: get sparse representations of policy rules, and distance between neighboring policy gridpoints - sspol_i = {} - sspol_pi = {} - sspol_space = {} - for pol in self.policy: - # use robust binary-search-based method that only requires grids to be monotonic - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord_robust(grid[pol], ss[pol]) - sspol_space[pol] = grid[pol][sspol_i[pol]+1] - grid[pol][sspol_i[pol]] - - return Pi, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, sspol_i, sspol_pi, sspol_space + return differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs '''Part 6: helper to extract inputs and potentially process them through hetinput''' @@ -665,6 +608,19 @@ def make_inputs(self, back_step_inputs_dict): print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') raise + def make_law_of_motion(self, ss): + exog = Markov(ss[self.exogenous], 0) + + if len(self.policy) == 1: + endog = lottery_1d(ss[self.policy[0]], ss[self.policy[0] + '_grid']) + else: + endog = lottery_2d(ss[self.policy[0]], ss[self.policy[1]], + ss[self.policy[0] + '_grid'], ss[self.policy[1] + '_grid']) + + law_of_motion = CombinedTransition([exog, endog]) + + return law_of_motion + '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' def forward_step(self, D, Pi_T, pol_i, pol_pi): diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py index 1ff748d..6496249 100644 --- a/src/sequence_jacobian/blocks/support/het_support.py +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -171,8 +171,7 @@ def expectations(self, X): def shockable(self, Dss): return ShockableMarkov(self.Pi, self.i, Dss) - def stationary(self, tol=1E-11, maxit=10_000): - pi_seed = getattr(self.Pi, 'pi_seed', None) + def stationary(self, pi_seed, tol=1E-11, maxit=10_000): return general_stationary(self.Pi, pi_seed, tol, maxit) @@ -207,6 +206,10 @@ def shockable(self, Dss): return ShockableCombinedTransition(shockable_stages) + def __getitem__(self, i): + return self.stages[i] + + Shock = Any ListTupleShocks = Union[List[Shock], Tuple[Shock]] diff --git a/src/sequence_jacobian/utilities/ordered_set.py b/src/sequence_jacobian/utilities/ordered_set.py index 6cee1bc..7d4a7ff 100644 --- a/src/sequence_jacobian/utilities/ordered_set.py +++ b/src/sequence_jacobian/utilities/ordered_set.py @@ -29,6 +29,9 @@ def __contains__(self, k): def __len__(self): return len(self.d) + def __getitem__(self, i): + return list(self.d)[i] + def add(self, x): self.d[x] = None diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 97b5123..5c3711e 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -4,6 +4,9 @@ import numpy as np from sequence_jacobian import estimation +from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset +from sequence_jacobian import create_model + # See test_determinacy.py for the to-do describing this suppression From 89999ce3aaafc08a50df80907f9f833992bc386c Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 8 Sep 2021 10:12:21 -0500 Subject: [PATCH 249/288] completed preliminary refactoring of het_block methods (including nonlinear) with law_of_motion --- src/sequence_jacobian/blocks/het_block.py | 107 +++++----------------- 1 file changed, 25 insertions(+), 82 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 7125ba4..b137c03 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -141,49 +141,11 @@ def __repr__(self): def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000): - """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. - - Parameters - ---------- - backward_tol : [optional] float - in backward iteration, max abs diff between policy in consecutive steps needed for convergence - backward_maxit : [optional] int - maximum number of backward iterations, if 'backward_tol' not reached by then, raise error - forward_tol : [optional] float - in forward iteration, max abs diff between dist in consecutive steps needed for convergence - forward_maxit : [optional] int - maximum number of forward iterations, if 'forward_tol' not reached by then, raise error - - kwargs : dict - The following inputs are required as keyword arguments, which show up in 'kwargs': - - The exogenous Markov matrix, e.g. Pi=... if self.exogenous=='Pi' - - A seed for each backward variable, e.g. Va=... and Vb=... if self.back_iter_vars==('Va','Vb') - - A grid for each policy variable, e.g. a_grid=... and b_grid=... if self.policy==('a','b') - - All other inputs to the backward iteration function self.back_step_fun, except _p added to - for self.exogenous and self.back_iter_vars, for which the method uses steady-state values. - If there is a self.hetinput, then we need the inputs to that, not to self.back_step_fun. - - Other inputs in 'kwargs' are optional: - - A seed for the distribution: D=... - - If no seed for the distribution is provided, a seed for the invariant distribution - of the Markov process, e.g. Pi_seed=... if self.exogenous=='Pi' - - Returns - ---------- - ss : dict, contains - - ss inputs of self.back_step_fun and (if present) self.hetinput - - ss outputs of self.back_step_fun - - ss distribution 'D' (and end-of-period distribution 'Dbeg') - - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars - """ - ss = calibration.toplevel.copy() if self.hetinputs is not None: ss.update(self.hetinputs(ss)) # extract information from calibration - Pi = ss[self.exogenous] - grid = {k: ss[k + '_grid'] for k in self.policy} D_seed = ss.get('D', None) pi_seed = ss.get(self.exogenous + '_seed', None) @@ -209,7 +171,7 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, {self.name: {k: ss[k] for k in ss if k in self.internal}}) - def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindividual=False, grid_paths=None): + def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindividual=False): """Evaluate transitional dynamics for HetBlock given dynamic paths for `inputs`, assuming that we start and end in steady state `ss`, and that all inputs not specified in `inputs` are constant at their ss values. @@ -223,31 +185,16 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid to use faster interpolation routines, otherwise use slower robust to nonmonotonicity returnindividual : [optional] bool return distribution and full outputs on grid - grid_paths: [optional] dict of {str: array(T, Number of grid points)} - time-varying grids for policies """ ssin_dict = {**ss.toplevel, **ss.internal[self.name]} - Pi_T = ssin_dict[self.exogenous].T.copy() - D = ssin_dict['D'] + Dbeg = ssin_dict['Dbeg'] T = inputs.T - # construct grids for policy variables either from the steady state grid if the grid is meant to be - # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. - grid = {} - use_ss_grid = {} - for k in self.policy: - if grid_paths is not None and k in grid_paths: - grid[k] = grid_paths[k] - use_ss_grid[k] = False - else: - grid[k] = ssin_dict[k + "_grid"] - use_ss_grid[k] = True - # allocate empty arrays to store result, assume all like D toreturn = self.non_back_iter_outputs if self.hetoutputs is not None: toreturn = toreturn | self.hetoutputs.outputs - individual_paths = {k: np.empty((T,) + D.shape) for k in toreturn} + individual_paths = {k: np.empty((T,) + Dbeg.shape) for k in toreturn} # backward iteration backdict = dict(ssin_dict.items()) @@ -266,27 +213,23 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid for k in individual_paths: individual_paths[k][t, ...] = individual[k] - D_path = np.empty((T,) + D.shape) - D_path[0, ...] = D - for t in range(T-1): - # have to interpolate policy separately for each t to get sparse transition matrices - sspol_i = {} - sspol_pi = {} - for pol in self.policy: - if use_ss_grid[pol]: - grid_var = grid[pol] - else: - grid_var = grid[pol][t, ...] - if monotonic: - # TODO: change for two-asset case so assumption is monotonicity in own asset, not anything else - sspol_i[pol], sspol_pi[pol] = utils.interpolate.interpolate_coord(grid_var, - individual_paths[pol][t, ...]) - else: - sspol_i[pol], sspol_pi[pol] =\ - utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[pol][t, ...]) - - # step forward - D_path[t+1, ...] = self.forward_step(D_path[t, ...], Pi_T, sspol_i, sspol_pi) + Dbeg_path = np.empty((T,) + Dbeg.shape) + Dbeg_path[0, ...] = Dbeg + D_path = np.empty((T,) + Dbeg.shape) + + for t in range(T): + # assemble dict for this period's law of motion and make law of motion object + d = {k: individual_paths[k][t, ...] for k in self.policy} + d.update({k + '_grid': ssin_dict[k + '_grid'] for k in self.policy}) + d[self.exogenous] = ssin_dict[self.exogenous] + law_of_motion = self.make_law_of_motion(d) + + # now step forward in two, first exogenous this period then endogenous + D_path[t, ...] = law_of_motion[0].forward(Dbeg) + + if t < T-1: + Dbeg = law_of_motion[1].forward(D_path[t, ...]) + Dbeg_path[t+1, ...] = Dbeg # make this optional # obtain aggregates of all outputs, made uppercase aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) @@ -608,14 +551,14 @@ def make_inputs(self, back_step_inputs_dict): print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') raise - def make_law_of_motion(self, ss): - exog = Markov(ss[self.exogenous], 0) + def make_law_of_motion(self, d: dict): + exog = Markov(d[self.exogenous], 0) if len(self.policy) == 1: - endog = lottery_1d(ss[self.policy[0]], ss[self.policy[0] + '_grid']) + endog = lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid']) else: - endog = lottery_2d(ss[self.policy[0]], ss[self.policy[1]], - ss[self.policy[0] + '_grid'], ss[self.policy[1] + '_grid']) + endog = lottery_2d(d[self.policy[0]], d[self.policy[1]], + d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) law_of_motion = CombinedTransition([exog, endog]) From 53d78d72b3c8c9ed27402fef1cb38873556bd668 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 8 Sep 2021 10:53:07 -0500 Subject: [PATCH 250/288] implemented multiple Pi capability, existing single-Pi tests pass --- src/sequence_jacobian/blocks/het_block.py | 100 +++++++++--------- src/sequence_jacobian/utilities/discretize.py | 8 ++ tests/utils/test_markov.py | 19 ++++ 3 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 tests/utils/test_markov.py diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index b137c03..be638b1 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -62,9 +62,9 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.back_step_fun = ExtendedFunction(back_step_fun) - self.exogenous = exogenous + self.exogenous = OrderedSet(utils.misc.make_tuple(exogenous)) self.policy, self.back_iter_vars = (OrderedSet(utils.misc.make_tuple(x)) for x in (policy, backward)) - self.inputs_to_be_primed = self.back_iter_vars | [self.exogenous] + self.inputs_to_be_primed = self.back_iter_vars | self.exogenous self.non_back_iter_outputs = self.back_step_fun.outputs - self.back_iter_vars self.outputs = OrderedSet([o.capitalize() for o in self.non_back_iter_outputs]) @@ -72,7 +72,7 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] self.inputs.remove(exogenous + '_p') self.inputs.add(exogenous) - self.internal = OrderedSet(['D', 'Dbeg', self.exogenous]) | self.back_step_fun.outputs + self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.back_step_fun.outputs # store "original" copies of these for use whenever we process new hetinputs/hetoutputs self.original_inputs = self.inputs @@ -88,28 +88,29 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.process_hetinputs_hetoutputs(hetinputs, hetoutputs, tocopy=False) if len(self.policy) > 2: - raise ValueError(f"More than two endogenous policies in {back_step_fun.__name__}, not yet supported") + raise ValueError(f"More than two endogenous policies in {self.name}, not yet supported") # Checking that the various inputs/outputs attributes are correctly set - if self.exogenous + '_p' not in self.back_step_fun.inputs: - raise ValueError(f"Markov matrix '{self.exogenous}_p' not included as argument in {back_step_fun.__name__}") + for k in self.exogenous: + if k + '_p' not in self.back_step_fun.inputs: + raise ValueError(f"Markov matrix '{k}_p' not included as argument in {self.name}") for pol in self.policy: if pol not in self.back_step_fun.outputs: - raise ValueError(f"Policy '{pol}' not included as output in {back_step_fun.__name__}") + raise ValueError(f"Policy '{pol}' not included as output in {self.name}") if pol[0].isupper(): - raise ValueError(f"Policy '{pol}' is uppercase in {back_step_fun.__name__}, which is not allowed") + raise ValueError(f"Policy '{pol}' is uppercase in {self.name}, which is not allowed") for back in self.back_iter_vars: if back + '_p' not in self.back_step_fun.inputs: - raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") + raise ValueError(f"Backward variable '{back}_p' not included as argument in {self.name}") if back not in self.back_step_fun.outputs: - raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") + raise ValueError(f"Backward variable '{back}' not included as output in {self.name}") for out in self.non_back_iter_outputs: if out[0].isupper(): - raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") + raise ValueError("Output '{out}' is uppercase in {self.name}, which is not allowed") if backward_init is not None: backward_init = ExtendedFunction(backward_init) @@ -145,16 +146,12 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, if self.hetinputs is not None: ss.update(self.hetinputs(ss)) - # extract information from calibration - D_seed = ss.get('D', None) - pi_seed = ss.get(self.exogenous + '_seed', None) - # run backward iteration sspol = self.policy_ss(ss, tol=backward_tol, maxit=backward_maxit) ss.update(sspol) # run forward iteration - Dbeg, D = self.dist_ss(ss, forward_tol, forward_maxit, D_seed, pi_seed) + Dbeg, D = self.dist_ss(ss, forward_tol, forward_maxit) ss.update({'Dbeg': Dbeg, "D": D}) # run hetoutput if it's there @@ -221,14 +218,15 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid # assemble dict for this period's law of motion and make law of motion object d = {k: individual_paths[k][t, ...] for k in self.policy} d.update({k + '_grid': ssin_dict[k + '_grid'] for k in self.policy}) - d[self.exogenous] = ssin_dict[self.exogenous] - law_of_motion = self.make_law_of_motion(d) + d.update({k: ssin_dict[k] for k in self.exogenous}) + exog = self.make_exog_law_of_motion(d) + endog = self.make_endog_law_of_motion(d) # now step forward in two, first exogenous this period then endogenous - D_path[t, ...] = law_of_motion[0].forward(Dbeg) + D_path[t, ...] = exog.forward(Dbeg) if t < T-1: - Dbeg = law_of_motion[1].forward(D_path[t, ...]) + Dbeg = endog.forward(D_path[t, ...]) Dbeg_path[t+1, ...] = Dbeg # make this optional # obtain aggregates of all outputs, made uppercase @@ -249,12 +247,6 @@ def _impulse_linear(self, ss, inputs, outputs, Js): def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options - """ - Block-specific inputs - --------------------- - h : [optional] float - h for numerical differentiation of backward iteration - """ ss = {**ss.toplevel, **ss.internal[self.name]} if self.hetinputs is not None: ss.update(self.hetinputs(ss)) @@ -262,7 +254,9 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # step 0: preliminary processing of steady state differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h) - law_of_motion = self.make_law_of_motion(ss).shockable(ss['Dbeg']) + exog = self.make_exog_law_of_motion(ss) + endog = self.make_endog_law_of_motion(ss) + law_of_motion = CombinedTransition([exog, endog]).shockable(ss['Dbeg']) # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i @@ -372,7 +366,7 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): # run and store results of backward iteration, which come as tuple, in dict sspol = self.back_step_fun(ssin) except KeyError as e: - print(f'Missing input {e} to {self.back_step_fun.__name__}!') + print(f'Missing input {e} to {self.self.name}!') raise # only check convergence every 10 iterations for efficiency @@ -396,28 +390,34 @@ def policy_ss(self, ssin, tol=1E-8, maxit=5000): ssin[k] = original_ssin[k] return {**ssin, **sspol} - def dist_ss(self, ss, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): - law_of_motion = self.make_law_of_motion(ss) + def dist_ss(self, ss, tol=1E-10, maxit=100_000): + exog = self.make_exog_law_of_motion(ss) + endog = self.make_endog_law_of_motion(ss) + Dbeg_seed = ss.get('Dbeg', None) + pi_seeds = [ss.get(k + '_seed', None) for k in self.exogenous] + # first obtain initial distribution D - if D_seed is None: - # TODO: remember we need to change this once we have more than one exogenous - # compute stationary distribution for exogenous variable - pi = law_of_motion.stages[0].stationary(pi_seed) - - # now initialize full distribution with this, assuming uniform distribution on endogenous vars - endogenous_dims = [ss[k+'_grid'].shape[0] for k in self.policy] - D = np.tile(pi, endogenous_dims[::-1] + [1]).T / np.prod(endogenous_dims) + if Dbeg_seed is None: + # stationary distribution of each exogenous + pis = [exog[i].stationary(pi_seed) for i, pi_seed in enumerate(pi_seeds)] + + # uniform distribution over endogenous + endog_uniform = [np.full(len(ss[k+'_grid']), 1/len(ss[k+'_grid'])) for k in self.policy] + + # initialize outer product of all these as guess + Dbeg = utils.discretize.big_outer(pis + endog_uniform) else: - D = D_seed + Dbeg = Dbeg_seed # iterate until convergence by tol, or maxit + D = exog.forward(Dbeg) for it in range(maxit): - Dbeg_new = law_of_motion[1].forward(D) - D_new = law_of_motion[0].forward(Dbeg_new) + Dbeg_new = endog.forward(D) + D_new = exog.forward(Dbeg_new) # only check convergence every 10 iterations for efficiency - if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, D_new, tol): + if it % 10 == 0 and utils.optimized_routines.within_tolerance(Dbeg, Dbeg_new, tol): break Dbeg = Dbeg_new D = D_new @@ -425,7 +425,6 @@ def dist_ss(self, ss, tol=1E-10, maxit=100_000, D_seed=None, pi_seed=None): raise ValueError(f'No convergence after {maxit} forward iterations!') # "D" is after the exogenous shock, Dbeg is before it - D = law_of_motion.stages[0].forward(Dbeg) return Dbeg, D '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper @@ -548,22 +547,19 @@ def make_inputs(self, back_step_inputs_dict): try: return {k: input_dict[k] for k in self.back_step_fun.inputs if k in input_dict} except KeyError as e: - print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') + print(f'Missing backward variable or Markov matrix {e} for {self.self.name}!') raise - def make_law_of_motion(self, d: dict): - exog = Markov(d[self.exogenous], 0) + def make_exog_law_of_motion(self, d:dict): + return CombinedTransition([Markov(d[k], i) for i, k in enumerate(self.exogenous)]) + def make_endog_law_of_motion(self, d: dict): if len(self.policy) == 1: - endog = lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid']) + return lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid']) else: - endog = lottery_2d(d[self.policy[0]], d[self.policy[1]], + return lottery_2d(d[self.policy[0]], d[self.policy[1]], d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) - law_of_motion = CombinedTransition([exog, endog]) - - return law_of_motion - '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' def forward_step(self, D, Pi_T, pol_i, pol_pi): diff --git a/src/sequence_jacobian/utilities/discretize.py b/src/sequence_jacobian/utilities/discretize.py index 95ace8f..b79ae51 100644 --- a/src/sequence_jacobian/utilities/discretize.py +++ b/src/sequence_jacobian/utilities/discretize.py @@ -42,6 +42,14 @@ def stationary(Pi, pi_seed=None, tol=1E-11, maxit=10_000): return pi +def big_outer(pis): + """Return n-dimensional outer product of list of n vectors""" + pi = pis[0] + for pi_i in pis[1:]: + pi = np.kron(pi, pi_i) + return pi.reshape(*(len(pi_i) for pi_i in pis)) + + def mean(x, pi): """Mean of discretized random variable with support x and probability mass function pi.""" return np.sum(pi * x) diff --git a/tests/utils/test_markov.py b/tests/utils/test_markov.py new file mode 100644 index 0000000..be7d3d0 --- /dev/null +++ b/tests/utils/test_markov.py @@ -0,0 +1,19 @@ +from sequence_jacobian.utilities.discretize import big_outer +import numpy as np + +def test_2d(): + a = np.random.rand(10) + b = np.random.rand(12) + assert np.allclose(np.outer(a,b), big_outer([a,b])) + +def test_3d(): + a = np.array([1., 2]) + b = np.array([1., 7]) + small = np.outer(a, b) + + c = np.array([2., 4]) + product = np.empty((2,2,2)) + product[..., 0] = 2*small + product[..., 1] = 4*small + + assert np.array_equal(product, big_outer([a,b,c])) From 3cff66583babb2e6078d0e425d20ba202ecbb185 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 8 Sep 2021 11:25:22 -0500 Subject: [PATCH 251/288] added test, multiple Pis works! --- src/sequence_jacobian/blocks/het_block.py | 6 +- .../blocks/support/het_support.py | 15 +---- src/sequence_jacobian/utilities/multidim.py | 25 ++++++++ tests/base/test_multiexog.py | 60 +++++++++++++++++++ tests/base/test_workflow.py | 2 +- 5 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 src/sequence_jacobian/utilities/multidim.py create mode 100644 tests/base/test_multiexog.py diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index be638b1..bd291fc 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -70,8 +70,8 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.outputs = OrderedSet([o.capitalize() for o in self.non_back_iter_outputs]) self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] - self.inputs.remove(exogenous + '_p') - self.inputs.add(exogenous) + self.inputs -= [k + '_p' for k in self.exogenous] + self.inputs |= self.exogenous self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.back_step_fun.outputs # store "original" copies of these for use whenever we process new hetinputs/hetoutputs @@ -406,7 +406,7 @@ def dist_ss(self, ss, tol=1E-10, maxit=100_000): endog_uniform = [np.full(len(ss[k+'_grid']), 1/len(ss[k+'_grid'])) for k in self.policy] # initialize outer product of all these as guess - Dbeg = utils.discretize.big_outer(pis + endog_uniform) + Dbeg = utils.multidim.outer(pis + endog_uniform) else: Dbeg = Dbeg_seed diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py index 6496249..1ade9d7 100644 --- a/src/sequence_jacobian/blocks/support/het_support.py +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -2,6 +2,7 @@ from . import het_compiled from ...utilities.discretize import stationary as general_stationary from ...utilities.interpolate import interpolate_coord_robust +from ...utilities.multidim import multiply_ith_dimension from typing import Optional, Sequence, Any, List, Tuple, Union class Transition: @@ -139,20 +140,6 @@ def forward_shock(self, da1, da2, dgrid1=None, dgrid2=None): pi_shock1, pi_shock2).reshape(self.shape) -def multiply_ith_dimension(Pi, i, X): - """If Pi is a square matrix, multiply Pi times the ith dimension of X and return""" - X = X.swapaxes(0, i) - shape = X.shape - X = X.reshape((X.shape[0], -1)) - - # iterate forward using Pi - X = Pi @ X - - # reverse steps - X = X.reshape(shape) - return X.swapaxes(0, i) - - class Markov(Transition): def __init__(self, Pi, i): self.Pi = Pi diff --git a/src/sequence_jacobian/utilities/multidim.py b/src/sequence_jacobian/utilities/multidim.py new file mode 100644 index 0000000..251c7d9 --- /dev/null +++ b/src/sequence_jacobian/utilities/multidim.py @@ -0,0 +1,25 @@ +import numpy as np + + +def multiply_ith_dimension(Pi, i, X): + """If Pi is a square matrix, multiply Pi times the ith dimension of X and return""" + X = X.swapaxes(0, i) + shape = X.shape + X = X.reshape((X.shape[0], -1)) + + # iterate forward using Pi + X = Pi @ X + + # reverse steps + X = X.reshape(shape) + return X.swapaxes(0, i) + + +def outer(pis): + """Return n-dimensional outer product of list of n vectors""" + pi = pis[0] + for pi_i in pis[1:]: + pi = np.kron(pi, pi_i) + return pi.reshape(*(len(pi_i) for pi_i in pis)) + + diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py new file mode 100644 index 0000000..a0a9d75 --- /dev/null +++ b/tests/base/test_multiexog.py @@ -0,0 +1,60 @@ +import numpy as np +import sequence_jacobian as sj +from sequence_jacobian.utilities.multidim import multiply_ith_dimension +from sequence_jacobian import het + + +def household_init(a_grid, y, r, sigma): + c = np.maximum(1e-8, y[..., np.newaxis] + np.maximum(r, 0.04) * a_grid) + Va = (1 + r) * (c ** (-sigma)) + return Va + + +@het(exogenous=['Pi1', 'Pi2'], policy='a', backward='Va', backward_init=household_init) +def household_multidim(Va_p, Pi1_p, Pi2_p, a_grid, y, r, beta, sigma): + Va_p = multiply_ith_dimension(beta * Pi1_p, 0, Va_p) + uc_nextgrid = multiply_ith_dimension(Pi2_p, 1, Va_p) + + c_nextgrid = uc_nextgrid ** (-1 / sigma) + coh = (1 + r) * a_grid + y[..., np.newaxis] + a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) + a = np.maximum(a, a_grid[0]) + c = coh - a + uc = c ** (-sigma) + Va = (1 + r) * uc + + return Va, a, c + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household_onedim(Va_p, Pi_p, a_grid, y, r, beta, sigma): + uc_nextgrid = (beta * Pi_p) @ Va_p + c_nextgrid = uc_nextgrid ** (-1 / sigma) + coh = (1 + r) * a_grid[np.newaxis, :] + y[:, np.newaxis] + a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) + sj.utilities.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + uc = c ** (-sigma) + Va = (1 + r) * uc + + return Va, a, c + +def test_equivalence(): + calibration = dict(beta=0.95, r=0.01, sigma=2, a_grid = sj.utilities.discretize.agrid(1000, 50)) + + e1, _, Pi1 = sj.utilities.discretize.markov_rouwenhorst(rho=0.7, sigma=0.7, N=3) + e2, _, Pi2 = sj.utilities.discretize.markov_rouwenhorst(rho=0.3, sigma=0.5, N=3) + e_multidim = np.outer(e1, e2) + + e_onedim = np.kron(e1, e2) + Pi = np.kron(Pi1, Pi2) + + ss_multidim = household_multidim.steady_state({**calibration, 'y': e_multidim, 'Pi1': Pi1, 'Pi2': Pi2}) + ss_onedim = household_onedim.steady_state({**calibration, 'y': e_onedim, 'Pi': Pi}) + + assert np.isclose(ss_multidim['A'], ss_onedim['A']) and np.isclose(ss_multidim['C'], ss_onedim['C']) + + D_onedim = ss_onedim.internal['household_onedim']['D'] + D_multidim = ss_multidim.internal['household_multidim']['D'] + + assert np.allclose(D_onedim, D_multidim.reshape(*D_onedim.shape)) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index c961f1b..4e0ed02 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -2,7 +2,7 @@ import sequence_jacobian as sj from sequence_jacobian import het, simple, solved, combine, create_model from sequence_jacobian.blocks.support.impulse import ImpulseDict - +from sequence_jacobian.utilities.multidim import multiply_ith_dimension '''Part 1: Household block''' From c4a537fd3327a3e5941f0c54bf7d314363054c1b Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 8 Sep 2021 13:53:37 -0500 Subject: [PATCH 252/288] cleanup, moved everything to multidim --- src/sequence_jacobian/blocks/het_block.py | 14 -------------- src/sequence_jacobian/utilities/discretize.py | 8 -------- tests/base/test_multiexog.py | 5 +++++ tests/base/test_workflow.py | 1 - tests/utils/{test_markov.py => test_multidim.py} | 6 +++--- 5 files changed, 8 insertions(+), 26 deletions(-) rename tests/utils/{test_markov.py => test_multidim.py} (63%) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index bd291fc..ce81950 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -169,20 +169,6 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, {self.name: {k: ss[k] for k in ss if k in self.internal}}) def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindividual=False): - """Evaluate transitional dynamics for HetBlock given dynamic paths for `inputs`, - assuming that we start and end in steady state `ss`, and that all inputs not specified in - `inputs` are constant at their ss values. - - CANNOT provide time-varying Markov transition matrix for now. - - Block-specific inputs - --------------------- - monotonic : [optional] bool - flag indicating date-t policies are monotonic in same date-(t-1) policies, allows us - to use faster interpolation routines, otherwise use slower robust to nonmonotonicity - returnindividual : [optional] bool - return distribution and full outputs on grid - """ ssin_dict = {**ss.toplevel, **ss.internal[self.name]} Dbeg = ssin_dict['Dbeg'] T = inputs.T diff --git a/src/sequence_jacobian/utilities/discretize.py b/src/sequence_jacobian/utilities/discretize.py index b79ae51..95ace8f 100644 --- a/src/sequence_jacobian/utilities/discretize.py +++ b/src/sequence_jacobian/utilities/discretize.py @@ -42,14 +42,6 @@ def stationary(Pi, pi_seed=None, tol=1E-11, maxit=10_000): return pi -def big_outer(pis): - """Return n-dimensional outer product of list of n vectors""" - pi = pis[0] - for pi_i in pis[1:]: - pi = np.kron(pi, pi_i) - return pi.reshape(*(len(pi_i) for pi_i in pis)) - - def mean(x, pi): """Mean of discretized random variable with support x and probability mass function pi.""" return np.sum(pi * x) diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index a0a9d75..7814a71 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -58,3 +58,8 @@ def test_equivalence(): D_multidim = ss_multidim.internal['household_multidim']['D'] assert np.allclose(D_onedim, D_multidim.reshape(*D_onedim.shape)) + + J_multidim = household_multidim.jacobian(ss_multidim, inputs = ['r'], outputs=['A'], T=10) + J_onedim = household_onedim.jacobian(ss_onedim, inputs = ['r'], outputs=['A'], T=10) + + assert np.allclose(J_multidim['A','r'], J_onedim['A','r']) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 4e0ed02..790b5d9 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -2,7 +2,6 @@ import sequence_jacobian as sj from sequence_jacobian import het, simple, solved, combine, create_model from sequence_jacobian.blocks.support.impulse import ImpulseDict -from sequence_jacobian.utilities.multidim import multiply_ith_dimension '''Part 1: Household block''' diff --git a/tests/utils/test_markov.py b/tests/utils/test_multidim.py similarity index 63% rename from tests/utils/test_markov.py rename to tests/utils/test_multidim.py index be7d3d0..8b87ada 100644 --- a/tests/utils/test_markov.py +++ b/tests/utils/test_multidim.py @@ -1,10 +1,10 @@ -from sequence_jacobian.utilities.discretize import big_outer +from sequence_jacobian.utilities.multidim import outer import numpy as np def test_2d(): a = np.random.rand(10) b = np.random.rand(12) - assert np.allclose(np.outer(a,b), big_outer([a,b])) + assert np.allclose(np.outer(a,b), outer([a,b])) def test_3d(): a = np.array([1., 2]) @@ -16,4 +16,4 @@ def test_3d(): product[..., 0] = 2*small product[..., 1] = 4*small - assert np.array_equal(product, big_outer([a,b,c])) + assert np.array_equal(product, outer([a,b,c])) From 958ed466eeb912b95e4dc7940253dbe8c8eaabae Mon Sep 17 00:00:00 2001 From: Michael Cai Date: Wed, 8 Sep 2021 15:28:00 -0500 Subject: [PATCH 253/288] Create a standalone calibration_block module, shift helper block functionality into Block's .solve_steady_state method so all sub-classes can access it, and put helper block-based sorting methods back into graph --- .../auxiliary_blocks/calibration_block.py | 44 ++++ .../blocks/combined_block.py | 205 ------------------ src/sequence_jacobian/models/two_asset.py | 4 +- src/sequence_jacobian/primitives.py | 32 ++- src/sequence_jacobian/steady_state/support.py | 23 ++ src/sequence_jacobian/utilities/graph.py | 144 ++++++++++++ 6 files changed, 236 insertions(+), 216 deletions(-) create mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py new file mode 100644 index 0000000..5371f2b --- /dev/null +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py @@ -0,0 +1,44 @@ +"""A CombinedBlock sub-class specifically for steady state calibration with helper blocks""" + +from ..combined_block import CombinedBlock +from ...utilities.ordered_set import OrderedSet +from ...utilities.graph import block_sort_w_helpers, find_intermediate_inputs_w_helpers + + +class CalibrationBlock(CombinedBlock): + """A CalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering + the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an + CalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" + def __init__(self, blocks, helper_blocks, calibration, name=""): + sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) + intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) + + super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) + + self.helper_blocks = helper_blocks + self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) + + self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) + self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig + + def __repr__(self): + return f"" + + def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): + """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" + ss = calibration.copy() + helper_outputs = {} + for block in self.blocks: + if not evaluate_helpers and block in self.helper_blocks: + continue + # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children + inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] + outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) + if evaluate_helpers and block in self.helper_blocks: + helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) + ss.update(outputs) + else: + # Don't overwrite entries in ss_values corresponding to what has already + # been solved for in helper_blocks so we can check for consistency after-the-fact + ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) + return ss diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index af1a9cd..2f4bdb8 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -9,7 +9,6 @@ from ..utilities.ordered_set import OrderedSet from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from ..blocks.parent import Parent -from ..steady_state.support import subset_helper_block_unknowns from ..jacobian.classes import JacobianDict @@ -132,210 +131,6 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): return total_Js[original_outputs, :] - def solve_steady_state(self, calibration, unknowns, targets, solver="", helper_blocks=None, helper_targets=None, - **kwargs): - if helper_blocks is not None: - if helper_targets is None: - raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" - " in the `helper_targets` keyword argument.") - else: - targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - helper_targets = {t: targets[t] for t in helper_targets} if isinstance(helper_targets, list) else helper_targets - helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - - calibration_w_help = calibration.copy() - calibration_w_help.update(helper_targets) - ss_calibration_block = SSCalibrationBlock(self.blocks + helper_blocks, helper_blocks=helper_blocks, - calibration=calibration_w_help) - return ss_calibration_block.solve_steady_state(calibration_w_help, dict_diff(unknowns, helper_unknowns), - dict_diff(targets, helper_targets), solver=solver, - **kwargs) - else: - return super().solve_steady_state(calibration, unknowns, targets, solver=solver, **kwargs) - # Useful type aliases Model = CombinedBlock - -# A CombinedBlock sub-class specifically for steady state calibration with helper blocks -class SSCalibrationBlock(CombinedBlock): - """An SSCalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering - the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an - SSCalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" - def __init__(self, blocks, helper_blocks, calibration, name=""): - sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) - intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) - - super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) - - self.helper_blocks = helper_blocks - self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) - - self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) - self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig - - def __repr__(self): - return f"" - - def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): - """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" - ss = calibration.copy() - helper_outputs = {} - for block in self.blocks: - if not evaluate_helpers and block in self.helper_blocks: - continue - # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) - if evaluate_helpers and block in self.helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) - ss.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) - return ss - - -def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): - """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. - - Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's - inferred) that indicate their aggregate inputs and outputs - - Importantly, because including helper blocks in a blocks without additional measures - can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the - steady_state computation to resolve these cycles. - e.g. Consider Krusell Smith: - Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the helper block - because the "firm" block outputs "r", which the helper block takes as an input. - However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes - "K" as an input. - This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of helper block and the cycle would be resolved. - - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - helper_blocks: `list` - A list of helper blocks - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using helper blocks. Read above docstring for more detail - return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `blocks` - """ - if return_io: - # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, - helper_blocks=helper_blocks, calibration=calibration) - - # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, - helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(dep), inargs, outargs - else: - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) - - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(dep) - - -def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): - """Mirroring construct_output_map functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - - helper_inputs = set().union(*[block.inputs for block in helper_blocks]) - - outmap = dict() - outargs = set() - for num, block in enumerate(blocks): - # Find the relevant set of outputs corresponding to a block - if hasattr(block, "outputs"): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: - # Because some of the outputs of a helper block are, by construction, outputs that also appear in the - # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering - # throwing this ValueError - if o in outmap and block not in helper_blocks: - raise ValueError(f'{o} is output twice') - - # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share - # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap: - outmap[o] = num - if return_output_args and not (o in helper_inputs and o in calibration): - outargs.add(o) - else: - continue - if return_output_args: - return outmap, outargs - else: - return outmap - - -def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, - calibration=None, return_input_args=False, outargs=None): - """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - if outargs is None: - outargs = {} - - dep = {num: set() for num in range(len(blocks))} - inargs = set() - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - # Each potential input to a given block will either be 1) output by another block, - # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into - # the steady-state computation via the `calibration' dict. - # If the block is a helper block, then we want to check the calibration to see if the potential - # input is a pre-specified variable/parameter, and if it is then we will not add the block that - # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic - # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in helper_blocks): - dep[num].add(outmap[i]) - if return_input_args and not (i in outargs): - inargs.add(i) - if return_input_args: - return dep, inargs - else: - return dep - - -def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): - """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module - but augmented to support helper blocks""" - required = set() - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - if i in outmap: - required.add(i) - return required diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 99c6fc6..cfd5cd6 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -334,7 +334,7 @@ def partial_ss_step2(tax, w, U, N, muw, frisch): def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi2=2, epsI=4, omega=0.005, kappaw=0.1, phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, - verbose=True): + tol=1e-12, verbose=True): """Solve steady state of full GE model. Calibrate (beta, vphi, chi1, alpha, mup, Z) to hit targets for (r, tot_wealth, Bh, K, Y=N=1). """ @@ -375,7 +375,7 @@ def res(x): return np.array([asset_mkt, out['B'] - Bh]) # solve for beta, vphi, omega - (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), verbose=verbose) + (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), tol=tol, verbose=verbose) calibration['beta'], calibration['chi1'] = beta, chi1 # extra evaluation for reporting diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/primitives.py index 077ea94..d2981c9 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/primitives.py @@ -162,29 +162,43 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: Optional[List] = [], - solver: Optional[str] = "", solver_kwargs: Optional[Dict] = {}, helper_blocks: Optional[List] = [], helper_targets: Optional[Dict] = {}, - block_kwargs: Optional[Dict] = {}, ttol: Optional[float] = 1e-11, ctol: Optional[float] = 1e-9, + solver: Optional[str] = "", solver_kwargs: Optional[Dict] = {}, + block_kwargs: Optional[Dict] = {}, ttol: Optional[float] = 1e-12, ctol: Optional[float] = 1e-9, verbose: Optional[bool] = False, check_consistency: Optional[bool] = True, constrained_method: Optional[str] = "linear_continuation", constrained_kwargs: Optional[Dict] = {}): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" + if helper_blocks is not None: + if helper_targets is None: + raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" + " in the `helper_targets` keyword argument.") + else: + from .steady_state.support import augment_dag_w_helper_blocks + dag, ss, unknowns_to_solve, targets_to_solve = augment_dag_w_helper_blocks(self, calibration, unknowns, + targets, helper_blocks, + helper_targets) + else: + dag, ss, unknowns_to_solve, targets_to_solve = self, SteadyStateDict(calibration), unknowns, targets + solver = solver if solver else provide_solver_default(unknowns) - ss = SteadyStateDict(calibration) - def residual(unknown_values, evaluate_helpers=True): - ss.update(misc.smart_zip(unknowns.keys(), unknown_values)) - ss.update(self.steady_state(ss, dissolve=dissolve, helper_targets=helper_targets, - evaluate_helpers=evaluate_helpers, **block_kwargs)) + def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=targets_to_solve, + evaluate_helpers=True): + ss.update(misc.smart_zip(unknowns_keys, unknown_values)) + ss.update(dag.steady_state(ss, dissolve=dissolve, evaluate_helpers=evaluate_helpers, **block_kwargs)) return compute_target_values(targets, ss) - unknowns_solved = solve_for_unknowns(residual, unknowns, solver, solver_kwargs, tol=ttol, verbose=verbose, + + unknowns_solved = solve_for_unknowns(residual, unknowns_to_solve, solver, solver_kwargs, tol=ttol, verbose=verbose, constrained_method=constrained_method, constrained_kwargs=constrained_kwargs) + if helper_blocks and helper_targets and check_consistency: # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) - cresid = np.max(abs(residual(unknowns_solved.values(), evaluate_helpers=False))) + cresid = np.max(abs(residual(unknowns_solved.values(), unknowns_keys=unknowns_solved.keys(), + targets=targets, evaluate_helpers=False))) if cresid > ctol: raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" f" the consistency of the DAG without redirection.") diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/steady_state/support.py index c27679c..e909189 100644 --- a/src/sequence_jacobian/steady_state/support.py +++ b/src/sequence_jacobian/steady_state/support.py @@ -6,9 +6,32 @@ from numbers import Real from functools import partial +from .classes import SteadyStateDict from ..utilities import misc, solvers + +def augment_dag_w_helper_blocks(dag, calibration, unknowns, targets, helper_blocks, helper_targets): + """For a given DAG (either an individual Block or CombinedBlock), add a set of helper blocks, which help + to solve the provided set of helper targets analytically, reducing the number of unknowns/targets that need + to be solved for numerically. + """ + from ..blocks.auxiliary_blocks.calibration_block import CalibrationBlock + + targets = {t: 0. for t in targets} if isinstance(targets, list) else targets + helper_targets = {t: targets[t] for t in helper_targets} if isinstance(helper_targets, list) else helper_targets + helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) + + unknowns_to_solve = misc.dict_diff(unknowns, helper_unknowns) + targets_to_solve = misc.dict_diff(targets, helper_targets) + + ss = SteadyStateDict({**calibration, **helper_targets}) + blocks = dag.blocks if hasattr(dag, "blocks") else [dag] + dag_augmented = CalibrationBlock(blocks + helper_blocks, helper_blocks=helper_blocks, calibration=ss) + + return dag_augmented, ss, unknowns_to_solve, targets_to_solve + + def instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs): """Instantiate mutable types from `None` default values in the steady_state function""" if dissolve is None: diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index c091f70..15f632d 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -131,6 +131,150 @@ def find_intermediate_inputs(blocks, **kwargs): return required +def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): + """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. + + Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's + inferred) that indicate their aggregate inputs and outputs + + Importantly, because including helper blocks in a blocks without additional measures + can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the + steady_state computation to resolve these cycles. + e.g. Consider Krusell Smith: + Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). + Normally block_sort would include the "firm" block as a dependency of the helper block + because the "firm" block outputs "r", which the helper block takes as an input. + However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes + "K" as an input. + This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then + "firm" could be removed as a dependency of helper block and the cycle would be resolved. + + blocks: `list` + A list of the blocks (SimpleBlock, HetBlock, etc.) to sort + helper_blocks: `list` + A list of helper blocks + calibration: `dict` or `None` + An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles + introduced by using helper blocks. Read above docstring for more detail + return_io: `bool` + A boolean indicating whether to return the full set of input and output arguments from `blocks` + """ + if return_io: + # step 1: map outputs to blocks for topological sort + outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, + helper_blocks=helper_blocks, calibration=calibration) + + # step 2: dependency graph for topological sort and input list + dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, + helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep), inargs, outargs + else: + # step 1: map outputs to blocks for topological sort + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) + + # step 2: dependency graph for topological sort and input list + dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) + + return topological_sort(dep) + + +def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): + """Mirroring construct_output_map functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + + helper_inputs = set().union(*[block.inputs for block in helper_blocks]) + + outmap = dict() + outargs = set() + for num, block in enumerate(blocks): + # Find the relevant set of outputs corresponding to a block + if hasattr(block, "outputs"): + outputs = block.outputs + elif isinstance(block, dict): + outputs = block.keys() + else: + raise ValueError(f'{block} is not recognized as block or does not provide outputs') + + for o in outputs: + # Because some of the outputs of a helper block are, by construction, outputs that also appear in the + # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering + # throwing this ValueError + if o in outmap and block not in helper_blocks: + raise ValueError(f'{o} is output twice') + + # Priority sorting for standard blocks: + # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share + # a given output, such that the dependency graph is constructed on the standard blocks, where possible + if o not in outmap: + outmap[o] = num + if return_output_args and not (o in helper_inputs and o in calibration): + outargs.add(o) + else: + continue + if return_output_args: + return outmap, outargs + else: + return outmap + + +def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, + calibration=None, return_input_args=False, outargs=None): + """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support + helper blocks""" + if calibration is None: + calibration = {} + if helper_blocks is None: + helper_blocks = [] + if outargs is None: + outargs = {} + + dep = {num: set() for num in range(len(blocks))} + inargs = set() + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + # Each potential input to a given block will either be 1) output by another block, + # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into + # the steady-state computation via the `calibration' dict. + # If the block is a helper block, then we want to check the calibration to see if the potential + # input is a pre-specified variable/parameter, and if it is then we will not add the block that + # produces that input as an output as a dependency. + # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic + # dependency, if it were not for this resolution. + if i in outmap and not (i in calibration and block in helper_blocks): + dep[num].add(outmap[i]) + if return_input_args and not (i in outargs): + inargs.add(i) + if return_input_args: + return dep, inargs + else: + return dep + + +def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): + """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module + but augmented to support helper blocks""" + required = set() + outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) + for num, block in enumerate(blocks): + if hasattr(block, 'inputs'): + inputs = block.inputs + else: + inputs = set(i for o in block for i in block[o]) + for i in inputs: + if i in outmap: + required.add(i) + return required + + def complete_reverse_graph(gph): """Given directed graph represented as a dict from nodes to iterables of nodes, return representation of graph that is complete (i.e. has each vertex pointing to some iterable, even if empty), and a complete version of reversed too. From 3aecc162aab32d3fd02ff45211e9a8395d54a17c Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 8 Sep 2021 17:23:36 -0500 Subject: [PATCH 254/288] tests pass with code that is supposed to accommodate shocks to Pi, although we have not tested the actual Pi shocks yet --- src/sequence_jacobian/blocks/het_block.py | 241 ++++++++++-------- .../blocks/support/het_compiled.py | 4 +- .../blocks/support/het_support.py | 132 ++++++---- src/sequence_jacobian/models/hank.py | 4 +- src/sequence_jacobian/models/krusell_smith.py | 4 +- src/sequence_jacobian/models/two_asset.py | 6 +- tests/base/test_het_support.py | 32 +-- tests/base/test_multiexog.py | 15 +- tests/base/test_workflow.py | 5 +- 9 files changed, 244 insertions(+), 199 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index ce81950..b52ffd4 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -1,6 +1,6 @@ import copy import numpy as np -from typing import Optional +from typing import Optional, Dict from .support.impulse import ImpulseDict from .support.bijection import Bijection @@ -11,7 +11,7 @@ from .support.bijection import Bijection from ..utilities.function import ExtendedFunction, ExtendedParallelFunction from ..utilities.ordered_set import OrderedSet -from .support.het_support import ShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition +from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition def het(exogenous, policy, backward, backward_init=None): @@ -64,13 +64,11 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non self.exogenous = OrderedSet(utils.misc.make_tuple(exogenous)) self.policy, self.back_iter_vars = (OrderedSet(utils.misc.make_tuple(x)) for x in (policy, backward)) - self.inputs_to_be_primed = self.back_iter_vars | self.exogenous self.non_back_iter_outputs = self.back_step_fun.outputs - self.back_iter_vars self.outputs = OrderedSet([o.capitalize() for o in self.non_back_iter_outputs]) self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] - self.inputs -= [k + '_p' for k in self.exogenous] self.inputs |= self.exogenous self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.back_step_fun.outputs @@ -91,10 +89,6 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non raise ValueError(f"More than two endogenous policies in {self.name}, not yet supported") # Checking that the various inputs/outputs attributes are correctly set - for k in self.exogenous: - if k + '_p' not in self.back_step_fun.inputs: - raise ValueError(f"Markov matrix '{k}_p' not included as argument in {self.name}") - for pol in self.policy: if pol not in self.back_step_fun.outputs: raise ValueError(f"Policy '{pol}' not included as output in {self.name}") @@ -142,21 +136,18 @@ def __repr__(self): def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000): - ss = calibration.toplevel.copy() - if self.hetinputs is not None: - ss.update(self.hetinputs(ss)) + ss = self.extract_ss_dict(calibration) + self.update_with_hetinputs(ss) + self.initialize_backward(ss) # run backward iteration - sspol = self.policy_ss(ss, tol=backward_tol, maxit=backward_maxit) - ss.update(sspol) + ss = self.policy_ss(ss, tol=backward_tol, maxit=backward_maxit) # run forward iteration Dbeg, D = self.dist_ss(ss, forward_tol, forward_maxit) ss.update({'Dbeg': Dbeg, "D": D}) - # run hetoutput if it's there - if self.hetoutputs is not None: - ss.update(self.hetoutputs(ss)) + self.update_with_hetoutputs(ss) # aggregate all outputs other than backward variables on grid, capitalize toreturn = self.non_back_iter_outputs @@ -168,11 +159,14 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, {self.name: {k: ss[k] for k in ss if k in self.internal}}) - def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindividual=False): - ssin_dict = {**ss.toplevel, **ss.internal[self.name]} - Dbeg = ssin_dict['Dbeg'] + def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindividual=False): + ss = self.extract_ss_dict(ssin) + Dbeg = ss['Dbeg'] T = inputs.T + # for now not allowing shocks to exog, just trying to get code to work! + exog = self.make_exog_law_of_motion(ss) + # allocate empty arrays to store result, assume all like D toreturn = self.non_back_iter_outputs if self.hetoutputs is not None: @@ -180,21 +174,23 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid individual_paths = {k: np.empty((T,) + Dbeg.shape) for k in toreturn} # backward iteration - backdict = dict(ssin_dict.items()) + backdict = ss.copy() + for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ssin_dict[k] + v[t, ...] for k, v in inputs.items()}) - if self.hetinputs is not None: - backdict.update(self.hetinputs(backdict)) - individual = self.make_inputs(backdict) - individual.update(self.back_step_fun(individual)) - backdict.update({k: individual[k] for k in self.back_iter_vars}) + for k in self.back_iter_vars: + backdict[k + '_p'] = exog.expectation(backdict[k]) + del backdict[k] - if self.hetoutputs is not None: - individual.update(self.hetoutputs(individual)) + # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! + backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) + self.update_with_hetinputs(backdict) + backdict.update(self.back_step_fun(backdict)) + backdict.update({k: backdict[k] for k in self.back_iter_vars}) + self.update_with_hetoutputs(backdict) + for k in individual_paths: - individual_paths[k][t, ...] = individual[k] + individual_paths[k][t, ...] = backdict[k] Dbeg_path = np.empty((T,) + Dbeg.shape) Dbeg_path[0, ...] = Dbeg @@ -203,8 +199,8 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid for t in range(T): # assemble dict for this period's law of motion and make law of motion object d = {k: individual_paths[k][t, ...] for k in self.policy} - d.update({k + '_grid': ssin_dict[k + '_grid'] for k in self.policy}) - d.update({k: ssin_dict[k] for k in self.exogenous}) + d.update({k + '_grid': ss[k + '_grid'] for k in self.policy}) + d.update({k: ss[k] for k in self.exogenous}) exog = self.make_exog_law_of_motion(d) endog = self.make_endog_law_of_motion(d) @@ -222,9 +218,9 @@ def _impulse_nonlinear(self, ss, inputs, outputs, monotonic=False, returnindivid # return either this, or also include distributional information # TODO: rethink this if returnindividual: - return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ss + return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ssin else: - return ImpulseDict(aggregates)[outputs] - ss + return ImpulseDict(aggregates)[outputs] - ssin def _impulse_linear(self, ss, inputs, outputs, Js): @@ -233,16 +229,16 @@ def _impulse_linear(self, ss, inputs, outputs, Js): def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options - ss = {**ss.toplevel, **ss.internal[self.name]} - if self.hetinputs is not None: - ss.update(self.hetinputs(ss)) + ss = self.extract_ss_dict(ss) + self.update_with_hetinputs(ss) outputs = self.M_outputs.inv @ outputs # horrible # step 0: preliminary processing of steady state - differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h) exog = self.make_exog_law_of_motion(ss) endog = self.make_endog_law_of_motion(ss) - law_of_motion = CombinedTransition([exog, endog]).shockable(ss['Dbeg']) + differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h, exog) + law_of_motion = CombinedTransition([exog, endog]).forward_shockable(ss['Dbeg']) + exog_by_output = {k: exog.expectation_shockable(ss[k]) for k in outputs | self.back_iter_vars} # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i @@ -250,7 +246,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): for i in inputs: curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, T, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, - law_of_motion, ss['D']) + law_of_motion, exog_by_output) # step 2 of fake news algorithm # compute prediction vectors curlyP (forward iteration) for each outcome o @@ -319,62 +315,43 @@ def add_hetoutputs(self, functions): def remove_hetoutputs(self, names): return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.remove(names)) + def update_with_hetinputs(self, d): + if self.hetinputs is not None: + d.update(self.hetinputs(d)) + + def update_with_hetoutputs(self, d): + if self.hetoutputs is not None: + d.update(self.hetoutputs(d)) + '''Part 3: components of ss(): - policy_ss : backward iteration to get steady-state policies and other outcomes - dist_ss : forward iteration to get steady-state distribution and compute aggregates ''' - def policy_ss(self, ssin, tol=1E-8, maxit=5000): - """Find steady-state policies and backward variables through backward iteration until convergence. - - Parameters - ---------- - ssin : dict - all steady-state inputs to back_step_fun, including seed values for backward variables - tol : [optional] float - max diff between consecutive iterations of policy variables needed for convergence - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - - Returns - ---------- - sspol : dict - all steady-state outputs of backward iteration, combined with inputs to backward iteration - """ - - # find initial values for backward iteration - original_ssin = ssin - ssin = self.make_inputs(ssin) + def policy_ss(self, ss, tol=1E-8, maxit=5000): + ss = ss.copy() + exog = self.make_exog_law_of_motion(ss) old = {} for it in range(maxit): - try: - # run and store results of backward iteration, which come as tuple, in dict - sspol = self.back_step_fun(ssin) - except KeyError as e: - print(f'Missing input {e} to {self.self.name}!') - raise + for k in self.back_iter_vars: + ss[k + '_p'] = exog.expectation(ss[k]) + del ss[k] - # only check convergence every 10 iterations for efficiency - if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) + ss.update(self.back_step_fun(ss)) + + if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(ss[k], old[k], tol) for k in self.policy): break - # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration - old.update({k: sspol[k] for k in self.policy}) - ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) + old.update({k: ss[k] for k in self.policy}) else: raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') - # want to record inputs in ssin, but remove _p, add in hetinput inputs if there - for k in self.inputs_to_be_primed: - ssin[k] = ssin[k + '_p'] - del ssin[k + '_p'] - if self.hetinputs is not None: - for k in self.hetinputs.inputs: - if k in original_ssin: - ssin[k] = original_ssin[k] - return {**ssin, **sspol} + for k in self.back_iter_vars: + del ss[k + '_p'] + + return ss def dist_ss(self, ss, tol=1E-10, maxit=100_000): exog = self.make_exog_law_of_motion(ss) @@ -421,27 +398,52 @@ def dist_ss(self, ss, tol=1E-10, maxit=100_000): ''' def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, - differentiable_hetoutput, law_of_motion: ShockableTransition, Dss): + differentiable_hetoutput, law_of_motion: ForwardShockableTransition, + exog: Dict[str, ExpectationShockableTransition], maybe_exog_shock=False): + + Dbeg, D = law_of_motion[0].Dss, law_of_motion[1].Dss # shock perturbs outputs shocked_outputs = differentiable_back_step_fun.diff(din_dict) - curlyV = {k: shocked_outputs[k] for k in self.back_iter_vars} + curlyV = {k: law_of_motion[0].expectation(shocked_outputs[k]) for k in self.back_iter_vars} + + # if there might be a shock to exogenous processes, figure out what it is + if maybe_exog_shock: + shocks_to_exog = [din_dict.get(k, None) for k in self.exogenous] + else: + shocks_to_exog = None - # which affects the distribution tomorrow + # perturbation to exog and outputs outputs affects distribution tomorrow policy_shock = [shocked_outputs[k] for k in self.policy] - curlyD = law_of_motion.forward_shock([None, policy_shock]) - #curlyD = law_of_motion[0].forward(curlyDbeg) + if len(policy_shock) == 1: + policy_shock = policy_shock[0] + curlyD = law_of_motion.forward_shock([shocks_to_exog, policy_shock]) - # and the aggregate outcomes today + # and also affect aggregate outcomes today + # TODO: seems wrong if hetoutput can take in hetinputs, since those might separately change it? if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): shocked_outputs.update(differentiable_hetoutput.diff(shocked_outputs)) - curlyY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} + curlyY = {k: np.vdot(D, shocked_outputs[k]) for k in output_list} + + # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY + if maybe_exog_shock: + for k in curlyV: + shock = exog[k].expectation_shock(shocks_to_exog) + if shock is not None: + curlyV[k] += shock + + for k in curlyY: + shock = exog[k].expectation_shock(shocks_to_exog) + # maybe could be more efficient since we don't need to calculate pointwise? + if shock is not None: + curlyY[k] += np.vdot(Dbeg, shock) return curlyV, curlyD, curlyY def backward_iteration_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, - differentiable_hetinput, differentiable_hetoutput, law_of_motion: ShockableTransition, Dss): - """Iterate policy steps backward T times for a single shock.""" + differentiable_hetinput, differentiable_hetoutput, + law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): + if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: # if input_shocked is an input to hetinput, take numerical diff to get response din_dict = differentiable_hetinput.diff2({input_shocked: 1}) @@ -451,7 +453,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, T, differentia # contemporaneous response to unit scalar shock curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, differentiable_hetoutput, - law_of_motion, Dss) + law_of_motion, exog, True) # infer dimensions from this and initialize empty arrays curlyDs = np.empty((T,) + curlyD.shape) @@ -466,21 +468,21 @@ def backward_iteration_fakenews(self, input_shocked, output_list, T, differentia for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, output_list, differentiable_back_step_fun, differentiable_hetoutput, - law_of_motion, Dss) + law_of_motion, exog) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] return curlyYs, curlyDs - def forward_iteration_fakenews(self, o_ss, T, law_of_motion: ShockableTransition): + def forward_iteration_fakenews(self, o_ss, T, law_of_motion: ForwardShockableTransition): """Iterate transpose forward T steps to get full set of curlyEs for a given outcome.""" curlyEs = np.empty((T,) + o_ss.shape) # initialize with beginning-of-period expectation of policy - curlyEs[0, ...] = utils.misc.demean(law_of_motion[0].expectations(o_ss)) + curlyEs[0, ...] = utils.misc.demean(law_of_motion[0].expectation(o_ss)) for t in range(1, T): # we demean so that curlyEs converge to zero (better numerically), in theory no effect - curlyEs[t, ...] = utils.misc.demean(law_of_motion.expectations(curlyEs[t-1, ...])) + curlyEs[t, ...] = utils.misc.demean(law_of_motion.expectation(curlyEs[t-1, ...])) return curlyEs @staticmethod @@ -501,9 +503,7 @@ def J_from_F(F): '''Part 5: helpers for .jac and .ajac: preliminary processing''' - def jac_backward_prelim(self, ss, h): - differentiable_back_step_fun = self.back_step_fun.differentiable(self.make_inputs(ss), h=h) - + def jac_backward_prelim(self, ss, h, exog): differentiable_hetinputs = None if self.hetinputs is not None: differentiable_hetinputs = self.hetinputs.differentiable(ss) @@ -512,29 +512,44 @@ def jac_backward_prelim(self, ss, h): if self.hetoutputs is not None: differentiable_hetoutputs = self.hetoutputs.differentiable(ss) + ss = ss.copy() + for k in self.back_iter_vars: + ss[k + '_p'] = exog.expectation(ss[k]) + differentiable_back_step_fun = self.back_step_fun.differentiable(ss, h=h) + return differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs '''Part 6: helper to extract inputs and potentially process them through hetinput''' - def make_inputs(self, back_step_inputs_dict): - """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun.""" - if isinstance(back_step_inputs_dict, SteadyStateDict): - input_dict = {**back_step_inputs_dict.toplevel, **back_step_inputs_dict.internal[self.name]} + def extract_ss_dict(self, ss): + if isinstance(ss, SteadyStateDict): + ssnew = ss.toplevel.copy() + if self.name in ss.internal: + ssnew.update(ss.internal[self.name]) + return ssnew else: - input_dict = back_step_inputs_dict.copy() + return ss.copy() + + def initialize_backward(self, ss): + if not all(k in ss for k in self.back_iter_vars): + ss.update(self.backward_init(ss)) + + # def make_inputs(self, back_step_inputs_dict): + # """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun.""" + - if not all(k in input_dict for k in self.back_iter_vars): - input_dict.update(self.backward_init(input_dict)) + # if not all(k in input_dict for k in self.back_iter_vars): + # input_dict.update(self.backward_init(input_dict)) - for i_p in self.inputs_to_be_primed: - input_dict[i_p + "_p"] = input_dict[i_p] - del input_dict[i_p] + # for i_p in self.inputs_to_be_primed: + # input_dict[i_p + "_p"] = input_dict[i_p] + # del input_dict[i_p] - try: - return {k: input_dict[k] for k in self.back_step_fun.inputs if k in input_dict} - except KeyError as e: - print(f'Missing backward variable or Markov matrix {e} for {self.self.name}!') - raise + # try: + # return {k: input_dict[k] for k in self.back_step_fun.inputs if k in input_dict} + # except KeyError as e: + # print(f'Missing backward variable or Markov matrix {e} for {self.self.name}!') + # raise def make_exog_law_of_motion(self, d:dict): return CombinedTransition([Markov(d[k], i) for i, k in enumerate(self.exogenous)]) diff --git a/src/sequence_jacobian/blocks/support/het_compiled.py b/src/sequence_jacobian/blocks/support/het_compiled.py index ddb9efa..b5033de 100644 --- a/src/sequence_jacobian/blocks/support/het_compiled.py +++ b/src/sequence_jacobian/blocks/support/het_compiled.py @@ -18,7 +18,7 @@ def forward_policy_1d(D, x_i, x_pi): @njit -def expectations_policy_1d(X, x_i, x_pi): +def expectation_policy_1d(X, x_i, x_pi): nZ, nX = X.shape Xnew = np.zeros_like(X) for iz in range(nZ): @@ -64,7 +64,7 @@ def forward_policy_2d(D, x_i, y_i, x_pi, y_pi): @njit -def expectations_policy_2d(X, x_i, y_i, x_pi, y_pi): +def expectation_policy_2d(X, x_i, y_i, x_pi, y_pi): nZ, nX, nY = X.shape Xnew = np.empty_like(X) for iz in range(nZ): diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py index 1ade9d7..4c258c4 100644 --- a/src/sequence_jacobian/blocks/support/het_support.py +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -10,15 +10,17 @@ class Transition: def forward(self, D): pass - def expectations(self, X): + def expectation(self, X): pass - def shockable(self, Dss): - # return ShockableTransition + def forward_shockable(self, Dss): pass + def expectation_shockable(self, Xss): + raise NotImplementedError(f'Shockable expectation not implemented for {type(self)}') -class ShockableTransition(Transition): + +class ForwardShockableTransition(Transition): """Abstract class extending Transition, allowing us to find effect of shock to transition rule on one-period-ahead distribution. This functionality isn't included in the regular Transition because it requires knowledge of the incoming ("steady-state") distribution and also sometimes @@ -31,6 +33,12 @@ def forward_shock(self, shocks): pass +class ExpectationShockableTransition(Transition): + def expectation_shock(self, shocks): + pass + + + def lottery_1d(a, a_grid): return PolicyLottery1D(*interpolate_coord_robust(a_grid, a), a_grid) @@ -54,29 +62,22 @@ def __init__(self, i, pi, grid): def forward(self, D): return het_compiled.forward_policy_1d(D.reshape(self.flatshape), self.i, self.pi).reshape(self.shape) - def expectations(self, X): - return het_compiled.expectations_policy_1d(X.reshape(self.flatshape), self.i, self.pi).reshape(self.shape) + def expectation(self, X): + return het_compiled.expectation_policy_1d(X.reshape(self.flatshape), self.i, self.pi).reshape(self.shape) - def shockable(self, Dss): - return ShockablePolicyLottery1D(self.i.reshape(self.shape), self.pi.reshape(self.shape), + def forward_shockable(self, Dss): + return ForwardShockablePolicyLottery1D(self.i.reshape(self.shape), self.pi.reshape(self.shape), self.grid, Dss) -class ShockablePolicyLottery1D(PolicyLottery1D, ShockableTransition): +class ForwardShockablePolicyLottery1D(PolicyLottery1D, ForwardShockableTransition): def __init__(self, i, pi, grid, Dss): super().__init__(i, pi, grid) self.Dss = Dss.reshape(self.flatshape) self.space = grid[self.i+1] - grid[self.i] - def forward_shock(self, da, dgrid=None): - # TODO: think about da being None too for more general applications + def forward_shock(self, da): pi_shock = - da.reshape(self.flatshape) / self.space - - if dgrid is not None: - # see "linearizing_interpolation" note - dgrid = np.broadcast_to(dgrid, self.shape) - pi_shock += self.expectations(dgrid).reshape(self.flatshape) / self.space - return het_compiled.forward_policy_shock_1d(self.Dss, self.i, pi_shock).reshape(self.shape) @@ -107,35 +108,28 @@ def forward(self, D): return het_compiled.forward_policy_2d(D.reshape(self.flatshape), self.i1, self.i2, self.pi1, self.pi2).reshape(self.shape) - def expectations(self, X): - return het_compiled.expectations_policy_2d(X.reshape(self.flatshape), self.i1, self.i2, + def expectation(self, X): + return het_compiled.expectation_policy_2d(X.reshape(self.flatshape), self.i1, self.i2, self.pi1, self.pi2).reshape(self.shape) - def shockable(self, Dss): - return ShockablePolicyLottery2D(self.i1.reshape(self.shape), self.pi1.reshape(self.shape), + def forward_shockable(self, Dss): + return ForwardShockablePolicyLottery2D(self.i1.reshape(self.shape), self.pi1.reshape(self.shape), self.i2.reshape(self.shape), self.pi2.reshape(self.shape), self.grid1, self.grid2, Dss) -class ShockablePolicyLottery2D(PolicyLottery2D, ShockableTransition): +class ForwardShockablePolicyLottery2D(PolicyLottery2D, ForwardShockableTransition): def __init__(self, i1, pi1, i2, pi2, grid1, grid2, Dss): super().__init__(i1, pi1, i2, pi2, grid1, grid2) self.Dss = Dss.reshape(self.flatshape) self.space1 = grid1[self.i1+1] - grid1[self.i1] self.space2 = grid2[self.i2+1] - grid2[self.i2] - def forward_shock(self, da1, da2, dgrid1=None, dgrid2=None): + def forward_shock(self, da): + da1, da2 = da pi_shock1 = -da1.reshape(self.flatshape) / self.space1 pi_shock2 = -da2.reshape(self.flatshape) / self.space2 - if dgrid1 is not None: - dgrid1 = np.broadcast_to(dgrid1[:, np.newaxis], self.shape) - pi_shock1 += self.expectations(dgrid1).reshape(self.flatshape) / self.space1 - - if dgrid2 is not None: - dgrid2 = np.broadcast_to(dgrid2, self.shape) - pi_shock2 += self.expectations(dgrid2).reshape(self.flatshape) / self.space2 - return het_compiled.forward_policy_shock_2d(self.Dss, self.i1, self.i2, self.pi1, self.pi2, pi_shock1, pi_shock2).reshape(self.shape) @@ -152,17 +146,20 @@ def __init__(self, Pi, i): def forward(self, D): return multiply_ith_dimension(self.Pi_T, self.i, D) - def expectations(self, X): + def expectation(self, X): return multiply_ith_dimension(self.Pi, self.i, X) - def shockable(self, Dss): - return ShockableMarkov(self.Pi, self.i, Dss) + def forward_shockable(self, Dss): + return ForwardShockableMarkov(self.Pi, self.i, Dss) + + def expectation_shockable(self, Xss): + return ExpectationShockableMarkov(self.Pi, self.i, Xss) def stationary(self, pi_seed, tol=1E-11, maxit=10_000): return general_stationary(self.Pi, pi_seed, tol, maxit) -class ShockableMarkov(Markov, ShockableTransition): +class ForwardShockableMarkov(Markov, ForwardShockableTransition): def __init__(self, Pi, i, Dss): super().__init__(Pi, i) self.Dss = Dss @@ -171,6 +168,15 @@ def forward_shock(self, dPi): return multiply_ith_dimension(dPi.T, self.i, self.Dss) +class ExpectationShockableMarkov(Markov, ExpectationShockableTransition): + def __init__(self, Pi, i, Xss): + super().__init__(Pi, i) + self.Xss = Xss + + def expectation_shock(self, dPi): + return multiply_ith_dimension(dPi, self.i, self.Xss) + + class CombinedTransition(Transition): def __init__(self, stages: Sequence[Transition]): self.stages = stages @@ -180,18 +186,26 @@ def forward(self, D): D = stage.forward(D) return D - def expectations(self, X): + def expectation(self, X): for stage in reversed(self.stages): - X = stage.expectations(X) + X = stage.expectation(X) return X - def shockable(self, Dss): + def forward_shockable(self, Dss): shockable_stages = [] for stage in self.stages: - shockable_stages.append(stage.shockable(Dss)) + shockable_stages.append(stage.forward_shockable(Dss)) Dss = stage.forward(Dss) - return ShockableCombinedTransition(shockable_stages) + return ForwardShockableCombinedTransition(shockable_stages) + + def expectation_shockable(self, Xss): + shockable_stages = [] + for stage in reversed(self.stages): + shockable_stages.append(stage.expectation_shockable(Xss)) + Xss = stage.expectation(Xss) + + return ExpectationShockableCombinedTransition(list(reversed(shockable_stages))) def __getitem__(self, i): return self.stages[i] @@ -200,20 +214,21 @@ def __getitem__(self, i): Shock = Any ListTupleShocks = Union[List[Shock], Tuple[Shock]] -class ShockableCombinedTransition(CombinedTransition, ShockableTransition): - def __init__(self, stages: Sequence[ShockableTransition]): +class ForwardShockableCombinedTransition(CombinedTransition, ForwardShockableTransition): + def __init__(self, stages: Sequence[ForwardShockableTransition]): self.stages = stages + self.Dss = stages[0].Dss + + def forward_shock(self, shocks: Optional[Sequence[Optional[Union[Shock, ListTupleShocks]]]]): + if shocks is None: + return None - def forward_shock(self, shocks: Sequence[Optional[Union[Shock, ListTupleShocks]]]): # each entry of shocks is either a sequence (list or tuple) dD = None for stage, shock in zip(self.stages, shocks): if shock is not None: - if isinstance(shock, tuple) or isinstance(shock, list): - dD_shock = stage.forward_shock(*shock) - else: - dD_shock = stage.forward_shock(shock) + dD_shock = stage.forward_shock(shock) else: dD_shock = None @@ -226,3 +241,26 @@ def forward_shock(self, shocks: Sequence[Optional[Union[Shock, ListTupleShocks]] dD = dD_shock return dD + + +class ExpectationShockableCombinedTransition(CombinedTransition, ExpectationShockableTransition): + def __init__(self, stages: Sequence[ExpectationShockableTransition]): + self.stages = stages + self.Xss = stages[-1].Xss + + def expectation_shock(self, shocks: Sequence[Optional[Union[Shock, ListTupleShocks]]]): + dX = None + + for stage, shock in zip(reversed(self.stages), reversed(shocks)): + if shock is not None: + dX_shock = stage.expectation_shock(shock) + else: + dX_shock = None + + if dX is not None: + dX = stage.expectation(dX) + + if shock is not None: + dX += dX_shock + else: + dX = dX_shock diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py index 393967c..74f1cf4 100644 --- a/src/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -17,13 +17,13 @@ def household_init(a_grid, e_grid, r, w, eis, T): @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): +def household(Va_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): """Single backward iteration step using endogenous gridpoint method for households with separable CRRA utility.""" # this one is useful to do internally ws = w * e_grid # uc(z_t, a_t) - uc_nextgrid = (beta * Pi_p) @ Va_p + uc_nextgrid = beta * Va_p # c(z_t, a_t) and n(z_t, a_t) c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py index fddd2cd..5eeeb8b 100644 --- a/src/sequence_jacobian/models/krusell_smith.py +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -17,7 +17,7 @@ def household_init(a_grid, e_grid, r, w, eis): @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, e_grid, r, w, beta, eis): +def household(Va_p, a_grid, e_grid, r, w, beta, eis): """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. Parameters @@ -37,7 +37,7 @@ def household(Va_p, Pi_p, a_grid, e_grid, r, w, beta, eis): a : array (S*A), asset policy today c : array (S*A), consumption policy today """ - uc_nextgrid = (beta * Pi_p) @ Va_p + uc_nextgrid = beta * Va_p c_nextgrid = uc_nextgrid ** (-eis) coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 99c6fc6..ecd1c8b 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -21,7 +21,7 @@ def household_init(b_grid, a_grid, e_grid, eis, tax, w): @het(exogenous='Pi', policy=['b', 'a'], backward=['Vb', 'Va'], backward_init=household_init) # order as in grid! -def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): +def household(Va_p, Vb_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): # require that k is decreasing (new) assert k_grid[1] < k_grid[0], 'kappas in k_grid must be decreasing!' @@ -31,8 +31,8 @@ def household(Va_p, Vb_p, Pi_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, ei # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === # (take discounted expectation of tomorrow's value function) - Wb = matrix_times_first_dim(beta * Pi_p, Vb_p) - Wa = matrix_times_first_dim(beta * Pi_p, Va_p) + Wb = beta * Vb_p + Wa = beta * Va_p W_ratio = Wa / Wb # === STEP 3: a'(z, b', a) for UNCONSTRAINED === diff --git a/tests/base/test_het_support.py b/tests/base/test_het_support.py index 72d8fdc..cb834fa 100644 --- a/tests/base/test_het_support.py +++ b/tests/base/test_het_support.py @@ -13,7 +13,7 @@ def test_combined_markov(): markovs = [Markov(Pi, i) for i, Pi in enumerate(Pis)] combined = CombinedTransition(markovs) - Dout = combined.expectations(D) + Dout = combined.expectation(D) Dout_forward = combined.forward(D) D_kron = D.reshape((-1, D.shape[2])) @@ -39,12 +39,12 @@ def test_many_markov_shock(): Dout_dn = CombinedTransition([Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))]).forward(D) Dder = (Dout_up - Dout_dn) / (2*h) - Dder2 = CombinedTransition([Markov(Pi, i) for i, Pi in enumerate(Pis)]).shockable(D).forward_shock(dPis) + Dder2 = CombinedTransition([Markov(Pi, i) for i, Pi in enumerate(Pis)]).forward_shockable(D).forward_shock(dPis) assert np.allclose(Dder, Dder2) -def test_policy_and_grid_shock(): +def test_policy_shock(): shape = (3, 4, 30) grid = np.geomspace(0.5, 10, shape[-1]) np.random.seed(98765) @@ -57,13 +57,12 @@ def test_policy_and_grid_shock(): D = np.random.rand(*shape) da = np.random.rand(*shape) - dgrid = np.random.rand(len(grid)) h = 1E-5 - Dout_up = lottery_1d(a + h*da, grid + h*dgrid).forward(D) - Dout_dn = lottery_1d(a - h*da, grid - h*dgrid).forward(D) + Dout_up = lottery_1d(a + h*da, grid).forward(D) + Dout_dn = lottery_1d(a - h*da, grid).forward(D) Dder = (Dout_up - Dout_dn) / (2*h) - Dder2 = lottery_1d(a, grid).shockable(D).forward_shock(da, dgrid) + Dder2 = lottery_1d(a, grid).forward_shockable(D).forward_shock(da) assert np.allclose(Dder, Dder2, atol=1E-4) @@ -83,12 +82,11 @@ def test_law_of_motion_shock(): Pis = [np.random.rand(s, s) for s in shape[:2]] da = np.random.rand(*shape) - dgrid = np.random.rand(len(grid)) dPis = [np.random.rand(s, s) for s in shape[:2]] h = 1E-5 - policy_up = lottery_1d(a + h*da, grid + h*dgrid) - policy_dn = lottery_1d(a - h*da, grid - h*dgrid) + policy_up = lottery_1d(a + h*da, grid) + policy_dn = lottery_1d(a - h*da, grid) markovs_up = [Markov(Pi + h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] markovs_dn =[Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] Dout_up = CombinedTransition([policy_up, *markovs_up]).forward(D) @@ -96,12 +94,12 @@ def test_law_of_motion_shock(): Dder = (Dout_up - Dout_dn) / (2*h) markovs = [Markov(Pi, i) for i, Pi, in enumerate(Pis)] - Dder2 = CombinedTransition([lottery_1d(a, grid), *markovs]).shockable(D).forward_shock([(da, dgrid), *dPis]) + Dder2 = CombinedTransition([lottery_1d(a, grid), *markovs]).forward_shockable(D).forward_shock([da, *dPis]) assert np.allclose(Dder, Dder2, atol=1E-4) -def test_2d_policy_and_grid_shock(): +def test_2d_policy_shock(): shape = (3, 4, 20, 30) a_grid = np.geomspace(0.5, 10, shape[-2]) b_grid = np.geomspace(0.2, 8, shape[-1]) @@ -119,14 +117,12 @@ def test_2d_policy_and_grid_shock(): da = np.random.rand(*shape) db = np.random.rand(*shape) - da_grid = np.random.rand(len(a_grid)) - db_grid = np.random.rand(len(b_grid)) dPis = [np.random.rand(s, s) for s in shape[:2]] h = 1E-5 - policy_up = lottery_2d(a + h*da, b + h*db, a_grid + h*da_grid, b_grid + h*db_grid) - policy_dn = lottery_2d(a - h*da, b - h*db, a_grid - h*da_grid, b_grid - h*db_grid) + policy_up = lottery_2d(a + h*da, b + h*db, a_grid, b_grid) + policy_dn = lottery_2d(a - h*da, b - h*db, a_grid, b_grid) markovs_up = [Markov(Pi + h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] markovs_dn = [Markov(Pi - h*dPi, i) for i, (Pi, dPi) in enumerate(zip(Pis, dPis))] Dout_up = CombinedTransition([policy_up, *markovs_up]).forward(D) @@ -136,7 +132,7 @@ def test_2d_policy_and_grid_shock(): policy = lottery_2d(a, b, a_grid, b_grid) markovs = [Markov(Pi, i) for i, Pi, in enumerate(Pis)] - Dder2 = CombinedTransition([policy, *markovs]).shockable(D).forward_shock([[da, db, da_grid, db_grid], *dPis]) + Dder2 = CombinedTransition([policy, *markovs]).forward_shockable(D).forward_shock([[da, db], *dPis]) assert np.allclose(Dder, Dder2, atol=1E-4) @@ -167,7 +163,7 @@ def test_forward_expectations_symmetry(): Xbackward = X for _ in range(30): - Xbackward = lom.expectations(Xbackward) + Xbackward = lom.expectation(Xbackward) outcome2 = np.vdot(D, Xbackward) assert np.isclose(outcome, outcome2) diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index 7814a71..d7e355a 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -11,11 +11,8 @@ def household_init(a_grid, y, r, sigma): @het(exogenous=['Pi1', 'Pi2'], policy='a', backward='Va', backward_init=household_init) -def household_multidim(Va_p, Pi1_p, Pi2_p, a_grid, y, r, beta, sigma): - Va_p = multiply_ith_dimension(beta * Pi1_p, 0, Va_p) - uc_nextgrid = multiply_ith_dimension(Pi2_p, 1, Va_p) - - c_nextgrid = uc_nextgrid ** (-1 / sigma) +def household_multidim(Va_p, a_grid, y, r, beta, sigma): + c_nextgrid = (beta*Va_p) ** (-1 / sigma) coh = (1 + r) * a_grid + y[..., np.newaxis] a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) a = np.maximum(a, a_grid[0]) @@ -25,11 +22,9 @@ def household_multidim(Va_p, Pi1_p, Pi2_p, a_grid, y, r, beta, sigma): return Va, a, c - @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household_onedim(Va_p, Pi_p, a_grid, y, r, beta, sigma): - uc_nextgrid = (beta * Pi_p) @ Va_p - c_nextgrid = uc_nextgrid ** (-1 / sigma) +def household_onedim(Va_p, a_grid, y, r, beta, sigma): + c_nextgrid = (beta * Va_p) ** (-1 / sigma) coh = (1 + r) * a_grid[np.newaxis, :] + y[:, np.newaxis] a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) sj.utilities.optimized_routines.setmin(a, a_grid[0]) @@ -63,3 +58,5 @@ def test_equivalence(): J_onedim = household_onedim.jacobian(ss_onedim, inputs = ['r'], outputs=['A'], T=10) assert np.allclose(J_multidim['A','r'], J_onedim['A','r']) + +test_equivalence() diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 790b5d9..f8cae16 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -13,7 +13,7 @@ def household_init(a_grid, y, rpost, sigma): @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, Pi_p, a_grid, y, rpost, beta, sigma): +def household(Va_p, a_grid, y, rpost, beta, sigma): """ Backward step in simple incomplete market model. Assumes CRRA utility. Parameters @@ -31,8 +31,7 @@ def household(Va_p, Pi_p, a_grid, y, rpost, beta, sigma): a : array (E, A), asset policy today c : array (E, A), consumption policy today """ - uc_nextgrid = (beta * Pi_p) @ Va_p - c_nextgrid = uc_nextgrid ** (-1 / sigma) + c_nextgrid = (beta * Va_p) ** (-1 / sigma) coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) sj.utilities.optimized_routines.setmin(a, a_grid[0]) From 5a8942e292d1e64e462787089d0cc3f2693d5a6b Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 9 Sep 2021 10:38:49 -0400 Subject: [PATCH 255/288] debugging jacobian wrt Pi shocks in progress --- src/sequence_jacobian/blocks/het_block.py | 3 +- tests/base/test_multiexog.py | 51 ++++++++++++++++++++--- tests/base/test_workflow.py | 18 -------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index b52ffd4..605e755 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -428,7 +428,7 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY if maybe_exog_shock: for k in curlyV: - shock = exog[k].expectation_shock(shocks_to_exog) + shock = exog[k].expectation_shock(shocks_to_exog) # this does not work if shock is not None: curlyV[k] += shock @@ -444,6 +444,7 @@ def backward_iteration_fakenews(self, input_shocked, output_list, T, differentia differentiable_hetinput, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): + # TODO: what if input_shocked enters both hetinput and backward_step_fun? if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: # if input_shocked is an input to hetinput, take numerical diff to get response din_dict = differentiable_hetinput.diff2({input_shocked: 1}) diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index d7e355a..456505c 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -1,7 +1,7 @@ import numpy as np import sequence_jacobian as sj from sequence_jacobian.utilities.multidim import multiply_ith_dimension -from sequence_jacobian import het +from sequence_jacobian import het, simple, combine def household_init(a_grid, y, r, sigma): @@ -10,9 +10,30 @@ def household_init(a_grid, y, r, sigma): return Va -@het(exogenous=['Pi1', 'Pi2'], policy='a', backward='Va', backward_init=household_init) +def search_frictions(f, s): + Pi_e = np.vstack(([1 - s, s], [f, 1 - f])) + return Pi_e + + +def labor_income(z, w, b): + y = np.vstack((w * z, b * w * z)) + return y + + +@simple +def income_state_vars(rho_z, sd_z, nZ): + z, _, Pi_z = sj.utilities.discretize.markov_rouwenhorst(rho=rho_z, sigma=sd_z, N=nZ) + return z, Pi_z + +@simple +def asset_state_vars(amin, amax, nA): + a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) + return a_grid + + +@het(exogenous=['Pi_e', 'Pi_z'], policy='a', backward='Va', backward_init=household_init) def household_multidim(Va_p, a_grid, y, r, beta, sigma): - c_nextgrid = (beta*Va_p) ** (-1 / sigma) + c_nextgrid = (beta * Va_p) ** (-1 / sigma) coh = (1 + r) * a_grid + y[..., np.newaxis] a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) a = np.maximum(a, a_grid[0]) @@ -44,7 +65,7 @@ def test_equivalence(): e_onedim = np.kron(e1, e2) Pi = np.kron(Pi1, Pi2) - ss_multidim = household_multidim.steady_state({**calibration, 'y': e_multidim, 'Pi1': Pi1, 'Pi2': Pi2}) + ss_multidim = household_multidim.steady_state({**calibration, 'y': e_multidim, 'Pi_z': Pi1, 'Pi_e': Pi2}) ss_onedim = household_onedim.steady_state({**calibration, 'y': e_onedim, 'Pi': Pi}) assert np.isclose(ss_multidim['A'], ss_onedim['A']) and np.isclose(ss_multidim['C'], ss_onedim['C']) @@ -59,4 +80,24 @@ def test_equivalence(): assert np.allclose(J_multidim['A','r'], J_onedim['A','r']) -test_equivalence() + +def test_pishock(): + calibration = dict(beta=0.95, r=0.01, sigma=2., f=0.4, s=0.1, w=1., b=0.5, + rho_z=0.9, sd_z=0.5, nZ=3, amin=0., amax=1000, nA=50) + + household = household_multidim.add_hetinputs([search_frictions, labor_income]) + hh = combine([household, income_state_vars, asset_state_vars]) + + ss = hh.steady_state(calibration) + + J = hh.jacobian(ss, inputs=['f', 's'], outputs=['C'], T=10) + + assert np.max(np.triu(J['C']['r']), 1) < 0 # low C before hike in r + assert np.min(np.tril(J['C']['r'])) > 0 # high C after hike in r + + # assert np.all(J['C']['f'] > 0) # high f increases C everywhere + # assert np.all(J['C']['s'] < 0) # high s decreases C everywhere + + return ss, J + +ss, J = test_pishock() \ No newline at end of file diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index f8cae16..719820e 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -14,23 +14,6 @@ def household_init(a_grid, y, rpost, sigma): @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) def household(Va_p, a_grid, y, rpost, beta, sigma): - """ - Backward step in simple incomplete market model. Assumes CRRA utility. - Parameters - ---------- - Va_p : array (E, A), marginal value of assets tomorrow (backward iterator) - Pi_p : array (E, E), Markov matrix for skills tomorrow - a_grid : array (A), asset grid - y : array (E), non-financial income - rpost : scalar, ex-post return on assets - beta : scalar, discount factor - sigma : scalar, utility parameter - Returns - ------- - Va : array (E, A), marginal value of assets today - a : array (E, A), asset policy today - c : array (E, A), consumption policy today - """ c_nextgrid = (beta * Va_p) ** (-1 / sigma) coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) @@ -167,4 +150,3 @@ def test_all(): inputs=shock, outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'Mpc') - From a34b34fd5c1589055c9374bd86f7331649abbab8 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 09:50:55 -0500 Subject: [PATCH 256/288] added return statement for expectation_shock, Jacobian for Pi shocks now seems to work --- .../blocks/support/het_support.py | 2 ++ tests/base/test_multiexog.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py index 4c258c4..c18c019 100644 --- a/src/sequence_jacobian/blocks/support/het_support.py +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -264,3 +264,5 @@ def expectation_shock(self, shocks: Sequence[Optional[Union[Shock, ListTupleShoc dX += dX_shock else: dX = dX_shock + + return dX \ No newline at end of file diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index 456505c..e6d8e81 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -65,7 +65,7 @@ def test_equivalence(): e_onedim = np.kron(e1, e2) Pi = np.kron(Pi1, Pi2) - ss_multidim = household_multidim.steady_state({**calibration, 'y': e_multidim, 'Pi_z': Pi1, 'Pi_e': Pi2}) + ss_multidim = household_multidim.steady_state({**calibration, 'y': e_multidim, 'Pi_e': Pi1, 'Pi_z': Pi2}) ss_onedim = household_onedim.steady_state({**calibration, 'y': e_onedim, 'Pi': Pi}) assert np.isclose(ss_multidim['A'], ss_onedim['A']) and np.isclose(ss_multidim['C'], ss_onedim['C']) @@ -90,14 +90,12 @@ def test_pishock(): ss = hh.steady_state(calibration) - J = hh.jacobian(ss, inputs=['f', 's'], outputs=['C'], T=10) + J = hh.jacobian(ss, inputs=['f', 's', 'r'], outputs=['C'], T=10) - assert np.max(np.triu(J['C']['r']), 1) < 0 # low C before hike in r - assert np.min(np.tril(J['C']['r'])) > 0 # high C after hike in r + assert np.max(np.triu(J['C']['r'], 1)) <= 0 # low C before hike in r + assert np.min(np.tril(J['C']['r'])) >= 0 # high C after hike in r - # assert np.all(J['C']['f'] > 0) # high f increases C everywhere - # assert np.all(J['C']['s'] < 0) # high s decreases C everywhere + assert np.all(J['C']['f'] > 0) # high f increases C everywhere + assert np.all(J['C']['s'] < 0) # high s decreases C everywhere return ss, J - -ss, J = test_pishock() \ No newline at end of file From 032d558bae57143296ad2b92099b250f67f4604d Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 10:17:22 -0500 Subject: [PATCH 257/288] impulse_nonlinear and jacobian now agree for shocks to exogenous --- src/sequence_jacobian/blocks/het_block.py | 103 ++++------------------ tests/base/test_multiexog.py | 6 +- 2 files changed, 21 insertions(+), 88 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 605e755..b0d4686 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -164,9 +164,6 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindiv Dbeg = ss['Dbeg'] T = inputs.T - # for now not allowing shocks to exog, just trying to get code to work! - exog = self.make_exog_law_of_motion(ss) - # allocate empty arrays to store result, assume all like D toreturn = self.non_back_iter_outputs if self.hetoutputs is not None: @@ -176,33 +173,36 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindiv # backward iteration backdict = ss.copy() + # initialize exogenous law of motion with steady-state, need this to take exp of backward vars + exog = self.make_exog_law_of_motion(backdict) + all_exog = [] + for t in reversed(range(T)): for k in self.back_iter_vars: backdict[k + '_p'] = exog.expectation(backdict[k]) del backdict[k] - # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) self.update_with_hetinputs(backdict) backdict.update(self.back_step_fun(backdict)) - backdict.update({k: backdict[k] for k in self.back_iter_vars}) - self.update_with_hetoutputs(backdict) for k in individual_paths: individual_paths[k][t, ...] = backdict[k] + exog = self.make_exog_law_of_motion(backdict) + + all_exog.append(exog) + + all_exog = all_exog[::-1] + Dbeg_path = np.empty((T,) + Dbeg.shape) Dbeg_path[0, ...] = Dbeg D_path = np.empty((T,) + Dbeg.shape) for t in range(T): - # assemble dict for this period's law of motion and make law of motion object - d = {k: individual_paths[k][t, ...] for k in self.policy} - d.update({k + '_grid': ss[k + '_grid'] for k in self.policy}) - d.update({k: ss[k] for k in self.exogenous}) - exog = self.make_exog_law_of_motion(d) - endog = self.make_endog_law_of_motion(d) + endog = self.make_endog_law_of_motion({**ss, **{k: individual_paths[k][t, ...] for k in self.policy}}) + exog = all_exog[t] # now step forward in two, first exogenous this period then endogenous D_path[t, ...] = exog.forward(Dbeg) @@ -216,7 +216,7 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindiv for o in individual_paths} # return either this, or also include distributional information - # TODO: rethink this + # TODO: rethink this when dealing with internals if returnindividual: return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ssin else: @@ -420,9 +420,8 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step curlyD = law_of_motion.forward_shock([shocks_to_exog, policy_shock]) # and also affect aggregate outcomes today - # TODO: seems wrong if hetoutput can take in hetinputs, since those might separately change it? if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): - shocked_outputs.update(differentiable_hetoutput.diff(shocked_outputs)) + shocked_outputs.update(differentiable_hetoutput.diff({**shocked_outputs, **din_dict})) curlyY = {k: np.vdot(D, shocked_outputs[k]) for k in output_list} # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY @@ -444,13 +443,9 @@ def backward_iteration_fakenews(self, input_shocked, output_list, T, differentia differentiable_hetinput, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): - # TODO: what if input_shocked enters both hetinput and backward_step_fun? + din_dict = {input_shocked: 1} if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: - # if input_shocked is an input to hetinput, take numerical diff to get response - din_dict = differentiable_hetinput.diff2({input_shocked: 1}) - else: - # otherwise, we just have that one shock - din_dict = {input_shocked: 1} + din_dict.update(differentiable_hetinput.diff2({input_shocked: 1})) # contemporaneous response to unit scalar shock curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, differentiable_hetoutput, @@ -535,23 +530,6 @@ def initialize_backward(self, ss): if not all(k in ss for k in self.back_iter_vars): ss.update(self.backward_init(ss)) - # def make_inputs(self, back_step_inputs_dict): - # """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun.""" - - - # if not all(k in input_dict for k in self.back_iter_vars): - # input_dict.update(self.backward_init(input_dict)) - - # for i_p in self.inputs_to_be_primed: - # input_dict[i_p + "_p"] = input_dict[i_p] - # del input_dict[i_p] - - # try: - # return {k: input_dict[k] for k in self.back_step_fun.inputs if k in input_dict} - # except KeyError as e: - # print(f'Missing backward variable or Markov matrix {e} for {self.self.name}!') - # raise - def make_exog_law_of_motion(self, d:dict): return CombinedTransition([Markov(d[k], i) for i, k in enumerate(self.exogenous)]) @@ -562,52 +540,3 @@ def make_endog_law_of_motion(self, d: dict): return lottery_2d(d[self.policy[0]], d[self.policy[1]], d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) - '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' - - def forward_step(self, D, Pi_T, pol_i, pol_pi): - """Update distribution, calling on 1d and 2d-specific compiled routines. - - Parameters - ---------- - D : array, beginning-of-period distribution - Pi_T : array, transpose Markov matrix - pol_i : dict, indices on lower bracketing gridpoint for all in self.policy - pol_pi : dict, weights on lower bracketing gridpoint for all in self.policy - - Returns - ---------- - Dnew : array, beginning-of-next-period distribution - """ - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_1d(D, Pi_T, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_2d(D, Pi_T, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - def forward_step_transpose(self, D, Pi, pol_i, pol_pi): - """Transpose of forward_step (note: this takes Pi rather than Pi_T as argument!)""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_transpose_1d(D, Pi, pol_i[p], pol_pi[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_transpose_2d(D, Pi, pol_i[p1], pol_i[p2], pol_pi[p1], pol_pi[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - - def forward_step_shock(self, Dss, Pi_T, pol_i_ss, pol_pi_ss, pol_pi_shock): - """Forward_step linearized with respect to pol_pi""" - if len(self.policy) == 1: - p, = self.policy - return utils.forward_step.forward_step_shock_1d(Dss, Pi_T, pol_i_ss[p], pol_pi_shock[p]) - elif len(self.policy) == 2: - p1, p2 = self.policy - return utils.forward_step.forward_step_shock_2d(Dss, Pi_T, pol_i_ss[p1], pol_i_ss[p2], - pol_pi_ss[p1], pol_pi_ss[p2], - pol_pi_shock[p1], pol_pi_shock[p2]) - else: - raise ValueError(f"{len(self.policy)} policy variables, only up to 2 implemented!") - diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index e6d8e81..1aea6ce 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -98,4 +98,8 @@ def test_pishock(): assert np.all(J['C']['f'] > 0) # high f increases C everywhere assert np.all(J['C']['s'] < 0) # high s decreases C everywhere - return ss, J + shock = 0.8**np.arange(10) + C_up = hh.impulse_nonlinear(ss, {'f': 1E-4*shock})['C'] + C_dn = hh.impulse_nonlinear(ss, {'f': -1E-4*shock})['C'] + dC = (C_up - C_dn)/2E-4 + assert np.allclose(dC, J['C', 'f'] @ shock, atol=2E-6) From 0ce8207c52ebaa8a26ca9fc22df637969fc7aa31 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 10:29:34 -0500 Subject: [PATCH 258/288] some rearranging / renaming in het_block.py for readability --- src/sequence_jacobian/blocks/het_block.py | 129 ++++++++++------------ 1 file changed, 61 insertions(+), 68 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index b0d4686..b63e4b2 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -11,7 +11,7 @@ from .support.bijection import Bijection from ..utilities.function import ExtendedFunction, ExtendedParallelFunction from ..utilities.ordered_set import OrderedSet -from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition +from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition, Transition def het(exogenous, policy, backward, backward_init=None): @@ -140,11 +140,8 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, self.update_with_hetinputs(ss) self.initialize_backward(ss) - # run backward iteration - ss = self.policy_ss(ss, tol=backward_tol, maxit=backward_maxit) - - # run forward iteration - Dbeg, D = self.dist_ss(ss, forward_tol, forward_maxit) + ss = self.backward_steady_state(ss, tol=backward_tol, maxit=backward_maxit) + Dbeg, D = self.forward_steady_state(ss, forward_tol, forward_maxit) ss.update({'Dbeg': Dbeg, "D": D}) self.update_with_hetoutputs(ss) @@ -244,15 +241,15 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in inputs: - curlyYs[i], curlyDs[i] = self.backward_iteration_fakenews(i, outputs, T, differentiable_back_step_fun, + curlyYs[i], curlyDs[i] = self.backward_fakenews(i, outputs, T, differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs, law_of_motion, exog_by_output) # step 2 of fake news algorithm - # compute prediction vectors curlyP (forward iteration) for each outcome o + # compute expectation vectors curlyE for each outcome o curlyPs = {} for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss[o], T-1, law_of_motion) + curlyPs[o] = self.expectation_vectors(ss[o], T-1, law_of_motion) # steps 3-4 of fake news algorithm # make fake news matrix and Jacobian for each outcome-input pair @@ -323,12 +320,10 @@ def update_with_hetoutputs(self, d): if self.hetoutputs is not None: d.update(self.hetoutputs(d)) - '''Part 3: components of ss(): - - policy_ss : backward iteration to get steady-state policies and other outcomes - - dist_ss : forward iteration to get steady-state distribution and compute aggregates - ''' + '''Steady-state backward and forward methods''' - def policy_ss(self, ss, tol=1E-8, maxit=5000): + def backward_steady_state(self, ss, tol=1E-8, maxit=5000): + """Backward iteration to get steady-state policies and other outcomes""" ss = ss.copy() exog = self.make_exog_law_of_motion(ss) @@ -353,7 +348,8 @@ def policy_ss(self, ss, tol=1E-8, maxit=5000): return ss - def dist_ss(self, ss, tol=1E-10, maxit=100_000): + def forward_steady_state(self, ss, tol=1E-10, maxit=100_000): + """Forward iteration to get steady-state distribution""" exog = self.make_exog_law_of_motion(ss) endog = self.make_endog_law_of_motion(ss) @@ -390,58 +386,12 @@ def dist_ss(self, ss, tol=1E-10, maxit=100_000): # "D" is after the exogenous shock, Dbeg is before it return Dbeg, D - '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper - - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs - - Step 2: forward_iteration_fakenews to get curlyPs - - Step 3: build_F to get fake news matrix from curlyYs, curlyDs, curlyPs - - Step 4: J_from_F to get Jacobian from fake news matrix - ''' - - def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, - differentiable_hetoutput, law_of_motion: ForwardShockableTransition, - exog: Dict[str, ExpectationShockableTransition], maybe_exog_shock=False): - - Dbeg, D = law_of_motion[0].Dss, law_of_motion[1].Dss - - # shock perturbs outputs - shocked_outputs = differentiable_back_step_fun.diff(din_dict) - curlyV = {k: law_of_motion[0].expectation(shocked_outputs[k]) for k in self.back_iter_vars} - - # if there might be a shock to exogenous processes, figure out what it is - if maybe_exog_shock: - shocks_to_exog = [din_dict.get(k, None) for k in self.exogenous] - else: - shocks_to_exog = None + '''Jacobian calculation: four parts of fake news algorithm''' - # perturbation to exog and outputs outputs affects distribution tomorrow - policy_shock = [shocked_outputs[k] for k in self.policy] - if len(policy_shock) == 1: - policy_shock = policy_shock[0] - curlyD = law_of_motion.forward_shock([shocks_to_exog, policy_shock]) - - # and also affect aggregate outcomes today - if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): - shocked_outputs.update(differentiable_hetoutput.diff({**shocked_outputs, **din_dict})) - curlyY = {k: np.vdot(D, shocked_outputs[k]) for k in output_list} - - # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY - if maybe_exog_shock: - for k in curlyV: - shock = exog[k].expectation_shock(shocks_to_exog) # this does not work - if shock is not None: - curlyV[k] += shock - - for k in curlyY: - shock = exog[k].expectation_shock(shocks_to_exog) - # maybe could be more efficient since we don't need to calculate pointwise? - if shock is not None: - curlyY[k] += np.vdot(Dbeg, shock) - - return curlyV, curlyD, curlyY - - def backward_iteration_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, + def backward_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): + """Part 1 of fake news algorithm: calculate curlyY and curlyD in response to fake news shock""" din_dict = {input_shocked: 1} if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: @@ -470,11 +420,11 @@ def backward_iteration_fakenews(self, input_shocked, output_list, T, differentia return curlyYs, curlyDs - def forward_iteration_fakenews(self, o_ss, T, law_of_motion: ForwardShockableTransition): - """Iterate transpose forward T steps to get full set of curlyEs for a given outcome.""" + def expectation_vectors(self, o_ss, T, law_of_motion: Transition): + """Part 2 of fake news algorithm: calculate expectation vectors curlyE""" curlyEs = np.empty((T,) + o_ss.shape) - # initialize with beginning-of-period expectation of policy + # initialize with beginning-of-period expectation of steady-state policy curlyEs[0, ...] = utils.misc.demean(law_of_motion[0].expectation(o_ss)) for t in range(1, T): # we demean so that curlyEs converge to zero (better numerically), in theory no effect @@ -483,6 +433,7 @@ def forward_iteration_fakenews(self, o_ss, T, law_of_motion: ForwardShockableTra @staticmethod def build_F(curlyYs, curlyDs, curlyEs): + """Part 3 of fake news algorithm: build fake news matrix from curlyY, curlyD, curlyE""" T = curlyDs.shape[0] Tpost = curlyEs.shape[0] - T + 2 F = np.empty((Tpost + T - 1, T)) @@ -492,12 +443,54 @@ def build_F(curlyYs, curlyDs, curlyEs): @staticmethod def J_from_F(F): + """Part 4 of fake news algorithm: recursively build Jacobian from fake news matrix""" J = F.copy() for t in range(1, J.shape[1]): J[1:, t] += J[:-1, t - 1] return J - '''Part 5: helpers for .jac and .ajac: preliminary processing''' + def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, + differentiable_hetoutput, law_of_motion: ForwardShockableTransition, + exog: Dict[str, ExpectationShockableTransition], maybe_exog_shock=False): + """Support for part 1 of fake news algorithm: single backward step in response to shock""" + + Dbeg, D = law_of_motion[0].Dss, law_of_motion[1].Dss + + # shock perturbs outputs + shocked_outputs = differentiable_back_step_fun.diff(din_dict) + curlyV = {k: law_of_motion[0].expectation(shocked_outputs[k]) for k in self.back_iter_vars} + + # if there might be a shock to exogenous processes, figure out what it is + if maybe_exog_shock: + shocks_to_exog = [din_dict.get(k, None) for k in self.exogenous] + else: + shocks_to_exog = None + + # perturbation to exog and outputs outputs affects distribution tomorrow + policy_shock = [shocked_outputs[k] for k in self.policy] + if len(policy_shock) == 1: + policy_shock = policy_shock[0] + curlyD = law_of_motion.forward_shock([shocks_to_exog, policy_shock]) + + # and also affect aggregate outcomes today + if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): + shocked_outputs.update(differentiable_hetoutput.diff({**shocked_outputs, **din_dict})) + curlyY = {k: np.vdot(D, shocked_outputs[k]) for k in output_list} + + # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY + if maybe_exog_shock: + for k in curlyV: + shock = exog[k].expectation_shock(shocks_to_exog) + if shock is not None: + curlyV[k] += shock + + for k in curlyY: + shock = exog[k].expectation_shock(shocks_to_exog) + # maybe could be more efficient since we don't need to calculate pointwise? + if shock is not None: + curlyY[k] += np.vdot(Dbeg, shock) + + return curlyV, curlyD, curlyY def jac_backward_prelim(self, ss, h, exog): differentiable_hetinputs = None From ebb878ee50d1dffa977e2790ae08b0624f5bce91 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 11:35:00 -0500 Subject: [PATCH 259/288] split impulse_nonlinear in het_block into backward and forward parts --- src/sequence_jacobian/blocks/het_block.py | 250 +++++++++++----------- 1 file changed, 122 insertions(+), 128 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index b63e4b2..049208d 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -123,17 +123,6 @@ def __repr__(self): else: return f"" - '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - - steady_state : do backward and forward iteration until convergence to get complete steady state - - impulse_nonlinear : do backward and forward iteration up to T to compute dynamics given some shocks - - impulse_linear : apply jacobians to compute linearized dynamics given some shocks - - jacobian : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - - - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput - - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after - each backward iteration step in td, jacobian is not computed for these! - ''' - def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, forward_tol=1E-10, forward_maxit=100_000): ss = self.extract_ss_dict(calibration) @@ -158,72 +147,31 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindividual=False): ss = self.extract_ss_dict(ssin) - Dbeg = ss['Dbeg'] - T = inputs.T - # allocate empty arrays to store result, assume all like D + # identify individual variable paths we want from backward iteration, then run it toreturn = self.non_back_iter_outputs if self.hetoutputs is not None: toreturn = toreturn | self.hetoutputs.outputs - individual_paths = {k: np.empty((T,) + Dbeg.shape) for k in toreturn} - - # backward iteration - backdict = ss.copy() - - # initialize exogenous law of motion with steady-state, need this to take exp of backward vars - exog = self.make_exog_law_of_motion(backdict) - all_exog = [] - - for t in reversed(range(T)): - for k in self.back_iter_vars: - backdict[k + '_p'] = exog.expectation(backdict[k]) - del backdict[k] - - backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) - self.update_with_hetinputs(backdict) - backdict.update(self.back_step_fun(backdict)) - self.update_with_hetoutputs(backdict) - - for k in individual_paths: - individual_paths[k][t, ...] = backdict[k] - - exog = self.make_exog_law_of_motion(backdict) - - all_exog.append(exog) - - all_exog = all_exog[::-1] - - Dbeg_path = np.empty((T,) + Dbeg.shape) - Dbeg_path[0, ...] = Dbeg - D_path = np.empty((T,) + Dbeg.shape) - - for t in range(T): - endog = self.make_endog_law_of_motion({**ss, **{k: individual_paths[k][t, ...] for k in self.policy}}) - exog = all_exog[t] + + individual_paths, exog_path = self.backward_nonlinear(ss, inputs, toreturn) - # now step forward in two, first exogenous this period then endogenous - D_path[t, ...] = exog.forward(Dbeg) - - if t < T-1: - Dbeg = endog.forward(D_path[t, ...]) - Dbeg_path[t+1, ...] = Dbeg # make this optional + # run forward iteration to get path of distribution (both Dbeg - what to do with this? - and D) + Dbeg_path, D_path = self.forward_nonlinear(ss, individual_paths, exog_path) # obtain aggregates of all outputs, made uppercase aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) for o in individual_paths} # return either this, or also include distributional information - # TODO: rethink this when dealing with internals + # TODO: rethink this when dealing with internals, including Dbeg if returnindividual: return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ssin else: return ImpulseDict(aggregates)[outputs] - ssin - def _impulse_linear(self, ss, inputs, outputs, Js): return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) - def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options ss = self.extract_ss_dict(ss) @@ -265,61 +213,6 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): return JacobianDict(J, name=self.name, T=T) - """HetInput and HetOutput processing""" - - def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunction], hetoutputs: Optional[ExtendedParallelFunction], tocopy=True): - if tocopy: - self = copy.copy(self) - inputs = self.original_inputs.copy() - outputs = self.original_outputs.copy() - internal = self.original_internal.copy() - - if hetoutputs is not None: - inputs |= (hetoutputs.inputs - self.back_step_fun.outputs - ['D']) - outputs |= [o.capitalize() for o in hetoutputs.outputs] - self.M_outputs = Bijection({o: o.capitalize() for o in hetoutputs.outputs}) @ self.original_M_outputs - internal |= hetoutputs.outputs - - if hetinputs is not None: - inputs |= hetinputs.inputs - inputs -= hetinputs.outputs - internal |= hetinputs.outputs - - self.inputs = inputs - self.outputs = outputs - self.internal = internal - - self.hetinputs = hetinputs - self.hetoutputs = hetoutputs - - return self - - def add_hetinputs(self, functions): - if self.hetinputs is None: - return self.process_hetinputs_hetoutputs(ExtendedParallelFunction(functions), self.hetoutputs) - else: - return self.process_hetinputs_hetoutputs(self.hetinputs.add(functions), self.hetoutputs) - - def remove_hetinputs(self, names): - return self.process_hetinputs_hetoutputs(self.hetinputs.remove(names), self.hetoutputs) - - def add_hetoutputs(self, functions): - if self.hetoutputs is None: - return self.process_hetinputs_hetoutputs(self.hetinputs, ExtendedParallelFunction(functions)) - else: - return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.add(functions)) - - def remove_hetoutputs(self, names): - return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.remove(names)) - - def update_with_hetinputs(self, d): - if self.hetinputs is not None: - d.update(self.hetinputs(d)) - - def update_with_hetoutputs(self, d): - if self.hetoutputs is not None: - d.update(self.hetoutputs(d)) - '''Steady-state backward and forward methods''' def backward_steady_state(self, ss, tol=1E-8, maxit=5000): @@ -386,35 +279,82 @@ def forward_steady_state(self, ss, tol=1E-10, maxit=100_000): # "D" is after the exogenous shock, Dbeg is before it return Dbeg, D - '''Jacobian calculation: four parts of fake news algorithm''' + '''Nonlinear impulse backward and forward methods''' + + def backward_nonlinear(self, ss, inputs, toreturn): + T = inputs.T + individual_paths = {k: np.empty((T,) + ss['D'].shape) for k in toreturn} + + backdict = ss.copy() + exog = self.make_exog_law_of_motion(backdict) + exog_path = [] + + for t in reversed(range(T)): + for k in self.back_iter_vars: + backdict[k + '_p'] = exog.expectation(backdict[k]) + del backdict[k] + + backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) + self.update_with_hetinputs(backdict) + backdict.update(self.back_step_fun(backdict)) + self.update_with_hetoutputs(backdict) + + for k in individual_paths: + individual_paths[k][t, ...] = backdict[k] + + exog = self.make_exog_law_of_motion(backdict) + + exog_path.append(exog) + + return individual_paths, exog_path[::-1] + + def forward_nonlinear(self, ss, individual_paths, exog_path): + T = len(exog_path) + Dbeg = ss['Dbeg'] + + Dbeg_path = np.empty((T,) + Dbeg.shape) + Dbeg_path[0, ...] = Dbeg + D_path = np.empty_like(Dbeg_path) + + for t in range(T): + endog = self.make_endog_law_of_motion({**ss, **{k: individual_paths[k][t, ...] for k in self.policy}}) + + # now step forward in two, first exogenous this period then endogenous + D_path[t, ...] = exog_path[t].forward(Dbeg) + + if t < T-1: + Dbeg = endog.forward(D_path[t, ...]) + Dbeg_path[t+1, ...] = Dbeg # make this optional + + return Dbeg_path, D_path + + '''Jacobian calculation: four parts of fake news algorithm, plus support methods''' def backward_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, differentiable_hetinput, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): """Part 1 of fake news algorithm: calculate curlyY and curlyD in response to fake news shock""" - + # contemporaneous effect of unit scalar shock to input_shocked din_dict = {input_shocked: 1} if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: din_dict.update(differentiable_hetinput.diff2({input_shocked: 1})) - # contemporaneous response to unit scalar shock - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, differentiable_hetoutput, - law_of_motion, exog, True) + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, + differentiable_hetoutput, law_of_motion, exog, True) - # infer dimensions from this and initialize empty arrays + # infer dimensions from this, initialize empty arrays, and fill in contemporaneous effect curlyDs = np.empty((T,) + curlyD.shape) curlyYs = {k: np.empty(T) for k in curlyY.keys()} - # fill in current effect of shock curlyDs[0, ...] = curlyD for k in curlyY.keys(): curlyYs[k][0] = curlyY[k] - # fill in anticipation effects + # fill in anticipation effects of shock up to horizon T for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, differentiable_back_step_fun, differentiable_hetoutput, - law_of_motion, exog) + output_list, differentiable_back_step_fun, + differentiable_hetoutput, law_of_motion, exog) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] @@ -427,7 +367,7 @@ def expectation_vectors(self, o_ss, T, law_of_motion: Transition): # initialize with beginning-of-period expectation of steady-state policy curlyEs[0, ...] = utils.misc.demean(law_of_motion[0].expectation(o_ss)) for t in range(1, T): - # we demean so that curlyEs converge to zero (better numerically), in theory no effect + # demean so that curlyEs converge to zero, in theory no effect but better numerically curlyEs[t, ...] = utils.misc.demean(law_of_motion.expectation(curlyEs[t-1, ...])) return curlyEs @@ -453,7 +393,6 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition], maybe_exog_shock=False): """Support for part 1 of fake news algorithm: single backward step in response to shock""" - Dbeg, D = law_of_motion[0].Dss, law_of_motion[1].Dss # shock perturbs outputs @@ -493,6 +432,7 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step return curlyV, curlyD, curlyY def jac_backward_prelim(self, ss, h, exog): + """Support for part 1 of fake news algorithm: preload differentiable functions""" differentiable_hetinputs = None if self.hetinputs is not None: differentiable_hetinputs = self.hetinputs.differentiable(ss) @@ -508,7 +448,62 @@ def jac_backward_prelim(self, ss, h, exog): return differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs - '''Part 6: helper to extract inputs and potentially process them through hetinput''' + '''HetInput and HetOutput options and processing''' + + def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunction], hetoutputs: Optional[ExtendedParallelFunction], tocopy=True): + if tocopy: + self = copy.copy(self) + inputs = self.original_inputs.copy() + outputs = self.original_outputs.copy() + internal = self.original_internal.copy() + + if hetoutputs is not None: + inputs |= (hetoutputs.inputs - self.back_step_fun.outputs - ['D']) + outputs |= [o.capitalize() for o in hetoutputs.outputs] + self.M_outputs = Bijection({o: o.capitalize() for o in hetoutputs.outputs}) @ self.original_M_outputs + internal |= hetoutputs.outputs + + if hetinputs is not None: + inputs |= hetinputs.inputs + inputs -= hetinputs.outputs + internal |= hetinputs.outputs + + self.inputs = inputs + self.outputs = outputs + self.internal = internal + + self.hetinputs = hetinputs + self.hetoutputs = hetoutputs + + return self + + def add_hetinputs(self, functions): + if self.hetinputs is None: + return self.process_hetinputs_hetoutputs(ExtendedParallelFunction(functions), self.hetoutputs) + else: + return self.process_hetinputs_hetoutputs(self.hetinputs.add(functions), self.hetoutputs) + + def remove_hetinputs(self, names): + return self.process_hetinputs_hetoutputs(self.hetinputs.remove(names), self.hetoutputs) + + def add_hetoutputs(self, functions): + if self.hetoutputs is None: + return self.process_hetinputs_hetoutputs(self.hetinputs, ExtendedParallelFunction(functions)) + else: + return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.add(functions)) + + def remove_hetoutputs(self, names): + return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.remove(names)) + + def update_with_hetinputs(self, d): + if self.hetinputs is not None: + d.update(self.hetinputs(d)) + + def update_with_hetoutputs(self, d): + if self.hetoutputs is not None: + d.update(self.hetoutputs(d)) + + '''Additional helper functions''' def extract_ss_dict(self, ss): if isinstance(ss, SteadyStateDict): @@ -531,5 +526,4 @@ def make_endog_law_of_motion(self, d: dict): return lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid']) else: return lottery_2d(d[self.policy[0]], d[self.policy[1]], - d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) - + d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) \ No newline at end of file From 0745610fd0ea394827889fd581a2cd94f3493644 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 11:58:08 -0500 Subject: [PATCH 260/288] some renaming, and also a switch to .upper() instead of .capitalize() for aggregation, in hetblock --- src/sequence_jacobian/blocks/het_block.py | 138 ++++++++-------------- src/sequence_jacobian/models/hank.py | 4 +- src/sequence_jacobian/models/two_asset.py | 4 +- tests/base/test_jacobian.py | 6 +- 4 files changed, 58 insertions(+), 94 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 049208d..45b62ac 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -15,62 +15,26 @@ def het(exogenous, policy, backward, backward_init=None): - def decorator(back_step_fun): - return HetBlock(back_step_fun, exogenous, policy, backward, backward_init=backward_init) + def decorator(backward_fun): + return HetBlock(backward_fun, exogenous, policy, backward, backward_init=backward_init) return decorator class HetBlock(Block): - """Part 1: Initializer for HetBlock, intended to be called via @het() decorator on backward step function. - - IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since - the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle - aggregation of non-aggregate outputs across the distribution and return aggregates as upper-case equivalents - of the `policy` and non-aggregate output variables specified in the backward step function. - """ - - def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=None, hetinputs=None, hetoutputs=None): - """Construct HetBlock from backward iteration function. - - Parameters - ---------- - back_step_fun : function - backward iteration function - exogenous : str - name of Markov transition matrix for exogenous variable - (now only single allowed for simplicity; use Kronecker product for more) - policy : str or sequence of str - names of policy variables of endogenous, continuous state variables - e.g. assets 'a', must be returned by function - backward : str or sequence of str - variables that together comprise the 'v' that we use for iterating backward - must appear both as outputs and as arguments - - It is assumed that every output of the function (except possibly backward), including policy, - will be on a grid of dimension 1 + len(policy), where the first dimension is the exogenous - variable and then the remaining dimensions are each of the continuous policy variables, in the - same order they are listed in 'policy'. - - The Markov transition matrix between the current and future period and backward iteration - variables should appear in the backward iteration function with '_p' subscripts ("prime") to - indicate that they come from the next period. - - Currently, we only support up to two policy variables. - """ - self.name = back_step_fun.__name__ + def __init__(self, backward_fun, exogenous, policy, backward, backward_init=None, hetinputs=None, hetoutputs=None): + self.backward_fun = ExtendedFunction(backward_fun) + self.name = self.backward_fun.name super().__init__() - self.back_step_fun = ExtendedFunction(back_step_fun) - self.exogenous = OrderedSet(utils.misc.make_tuple(exogenous)) - self.policy, self.back_iter_vars = (OrderedSet(utils.misc.make_tuple(x)) for x in (policy, backward)) - self.non_back_iter_outputs = self.back_step_fun.outputs - self.back_iter_vars + self.policy, self.backward = (OrderedSet(utils.misc.make_tuple(x)) for x in (policy, backward)) + self.non_backward_outputs = self.backward_fun.outputs - self.backward - self.outputs = OrderedSet([o.capitalize() for o in self.non_back_iter_outputs]) - self.M_outputs = Bijection({o: o.capitalize() for o in self.non_back_iter_outputs}) - self.inputs = self.back_step_fun.inputs - [k + '_p' for k in self.back_iter_vars] + self.outputs = OrderedSet([o.upper() for o in self.non_backward_outputs]) + self.M_outputs = Bijection({o: o.upper() for o in self.non_backward_outputs}) + self.inputs = self.backward_fun.inputs - [k + '_p' for k in self.backward] self.inputs |= self.exogenous - self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.back_step_fun.outputs + self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.backward_fun.outputs # store "original" copies of these for use whenever we process new hetinputs/hetoutputs self.original_inputs = self.inputs @@ -90,19 +54,19 @@ def __init__(self, back_step_fun, exogenous, policy, backward, backward_init=Non # Checking that the various inputs/outputs attributes are correctly set for pol in self.policy: - if pol not in self.back_step_fun.outputs: + if pol not in self.backward_fun.outputs: raise ValueError(f"Policy '{pol}' not included as output in {self.name}") if pol[0].isupper(): raise ValueError(f"Policy '{pol}' is uppercase in {self.name}, which is not allowed") - for back in self.back_iter_vars: - if back + '_p' not in self.back_step_fun.inputs: + for back in self.backward: + if back + '_p' not in self.backward_fun.inputs: raise ValueError(f"Backward variable '{back}_p' not included as argument in {self.name}") - if back not in self.back_step_fun.outputs: + if back not in self.backward_fun.outputs: raise ValueError(f"Backward variable '{back}' not included as output in {self.name}") - for out in self.non_back_iter_outputs: + for out in self.non_backward_outputs: if out[0].isupper(): raise ValueError("Output '{out}' is uppercase in {self.name}, which is not allowed") @@ -116,10 +80,10 @@ def __repr__(self): """Nice string representation of HetBlock for printing to console""" if self.hetinputs is not None: if self.hetoutputs is not None: - return f"" + return f"" else: - return f"" + return f"" else: return f"" @@ -136,10 +100,10 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, self.update_with_hetoutputs(ss) # aggregate all outputs other than backward variables on grid, capitalize - toreturn = self.non_back_iter_outputs + toreturn = self.non_backward_outputs if self.hetoutputs is not None: toreturn = toreturn | self.hetoutputs.outputs - aggregates = {o.capitalize(): np.vdot(D, ss[o]) for o in toreturn} + aggregates = {o.upper(): np.vdot(D, ss[o]) for o in toreturn} ss.update(aggregates) return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, @@ -149,7 +113,7 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindiv ss = self.extract_ss_dict(ssin) # identify individual variable paths we want from backward iteration, then run it - toreturn = self.non_back_iter_outputs + toreturn = self.non_backward_outputs if self.hetoutputs is not None: toreturn = toreturn | self.hetoutputs.outputs @@ -159,7 +123,7 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindiv Dbeg_path, D_path = self.forward_nonlinear(ss, individual_paths, exog_path) # obtain aggregates of all outputs, made uppercase - aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) + aggregates = {o.upper(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) for o in individual_paths} # return either this, or also include distributional information @@ -176,20 +140,20 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # TODO: h is unusable for now, figure out how to suggest options ss = self.extract_ss_dict(ss) self.update_with_hetinputs(ss) - outputs = self.M_outputs.inv @ outputs # horrible + outputs = self.M_outputs.inv @ outputs # step 0: preliminary processing of steady state exog = self.make_exog_law_of_motion(ss) endog = self.make_endog_law_of_motion(ss) - differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h, exog) + differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h, exog) law_of_motion = CombinedTransition([exog, endog]).forward_shockable(ss['Dbeg']) - exog_by_output = {k: exog.expectation_shockable(ss[k]) for k in outputs | self.back_iter_vars} + exog_by_output = {k: exog.expectation_shockable(ss[k]) for k in outputs | self.backward} # step 1 of fake news algorithm # compute curlyY and curlyD (backward iteration) for each input i curlyYs, curlyDs = {}, {} for i in inputs: - curlyYs[i], curlyDs[i] = self.backward_fakenews(i, outputs, T, differentiable_back_step_fun, + curlyYs[i], curlyDs[i] = self.backward_fakenews(i, outputs, T, differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs, law_of_motion, exog_by_output) @@ -204,12 +168,12 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): F, J = {}, {} for o in outputs: for i in inputs: - if o.capitalize() not in F: - F[o.capitalize()] = {} - if o.capitalize() not in J: - J[o.capitalize()] = {} - F[o.capitalize()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) - J[o.capitalize()][i] = HetBlock.J_from_F(F[o.capitalize()][i]) + if o.upper() not in F: + F[o.upper()] = {} + if o.upper() not in J: + J[o.upper()] = {} + F[o.upper()][i] = HetBlock.build_F(curlyYs[i][o], curlyDs[i], curlyPs[o]) + J[o.upper()][i] = HetBlock.J_from_F(F[o.upper()][i]) return JacobianDict(J, name=self.name, T=T) @@ -222,11 +186,11 @@ def backward_steady_state(self, ss, tol=1E-8, maxit=5000): old = {} for it in range(maxit): - for k in self.back_iter_vars: + for k in self.backward: ss[k + '_p'] = exog.expectation(ss[k]) del ss[k] - ss.update(self.back_step_fun(ss)) + ss.update(self.backward_fun(ss)) if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(ss[k], old[k], tol) for k in self.policy): @@ -236,7 +200,7 @@ def backward_steady_state(self, ss, tol=1E-8, maxit=5000): else: raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') - for k in self.back_iter_vars: + for k in self.backward: del ss[k + '_p'] return ss @@ -290,13 +254,13 @@ def backward_nonlinear(self, ss, inputs, toreturn): exog_path = [] for t in reversed(range(T)): - for k in self.back_iter_vars: + for k in self.backward: backdict[k + '_p'] = exog.expectation(backdict[k]) del backdict[k] backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) self.update_with_hetinputs(backdict) - backdict.update(self.back_step_fun(backdict)) + backdict.update(self.backward_fun(backdict)) self.update_with_hetoutputs(backdict) for k in individual_paths: @@ -330,7 +294,7 @@ def forward_nonlinear(self, ss, individual_paths, exog_path): '''Jacobian calculation: four parts of fake news algorithm, plus support methods''' - def backward_fakenews(self, input_shocked, output_list, T, differentiable_back_step_fun, + def backward_fakenews(self, input_shocked, output_list, T, differentiable_backward_fun, differentiable_hetinput, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition]): """Part 1 of fake news algorithm: calculate curlyY and curlyD in response to fake news shock""" @@ -339,7 +303,7 @@ def backward_fakenews(self, input_shocked, output_list, T, differentiable_back_s if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: din_dict.update(differentiable_hetinput.diff2({input_shocked: 1})) - curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_back_step_fun, + curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_backward_fun, differentiable_hetoutput, law_of_motion, exog, True) # infer dimensions from this, initialize empty arrays, and fill in contemporaneous effect @@ -353,7 +317,7 @@ def backward_fakenews(self, input_shocked, output_list, T, differentiable_back_s # fill in anticipation effects of shock up to horizon T for t in range(1, T): curlyV, curlyDs[t, ...], curlyY = self.backward_step_fakenews({k+'_p': v for k, v in curlyV.items()}, - output_list, differentiable_back_step_fun, + output_list, differentiable_backward_fun, differentiable_hetoutput, law_of_motion, exog) for k in curlyY.keys(): curlyYs[k][t] = curlyY[k] @@ -389,15 +353,15 @@ def J_from_F(F): J[1:, t] += J[:-1, t - 1] return J - def backward_step_fakenews(self, din_dict, output_list, differentiable_back_step_fun, + def backward_step_fakenews(self, din_dict, output_list, differentiable_backward_fun, differentiable_hetoutput, law_of_motion: ForwardShockableTransition, exog: Dict[str, ExpectationShockableTransition], maybe_exog_shock=False): """Support for part 1 of fake news algorithm: single backward step in response to shock""" Dbeg, D = law_of_motion[0].Dss, law_of_motion[1].Dss # shock perturbs outputs - shocked_outputs = differentiable_back_step_fun.diff(din_dict) - curlyV = {k: law_of_motion[0].expectation(shocked_outputs[k]) for k in self.back_iter_vars} + shocked_outputs = differentiable_backward_fun.diff(din_dict) + curlyV = {k: law_of_motion[0].expectation(shocked_outputs[k]) for k in self.backward} # if there might be a shock to exogenous processes, figure out what it is if maybe_exog_shock: @@ -442,11 +406,11 @@ def jac_backward_prelim(self, ss, h, exog): differentiable_hetoutputs = self.hetoutputs.differentiable(ss) ss = ss.copy() - for k in self.back_iter_vars: + for k in self.backward: ss[k + '_p'] = exog.expectation(ss[k]) - differentiable_back_step_fun = self.back_step_fun.differentiable(ss, h=h) + differentiable_backward_fun = self.backward_fun.differentiable(ss, h=h) - return differentiable_back_step_fun, differentiable_hetinputs, differentiable_hetoutputs + return differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs '''HetInput and HetOutput options and processing''' @@ -458,9 +422,9 @@ def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunct internal = self.original_internal.copy() if hetoutputs is not None: - inputs |= (hetoutputs.inputs - self.back_step_fun.outputs - ['D']) - outputs |= [o.capitalize() for o in hetoutputs.outputs] - self.M_outputs = Bijection({o: o.capitalize() for o in hetoutputs.outputs}) @ self.original_M_outputs + inputs |= (hetoutputs.inputs - self.backward_fun.outputs - ['D']) + outputs |= [o.upper() for o in hetoutputs.outputs] + self.M_outputs = Bijection({o: o.upper() for o in hetoutputs.outputs}) @ self.original_M_outputs internal |= hetoutputs.outputs if hetinputs is not None: @@ -515,7 +479,7 @@ def extract_ss_dict(self, ss): return ss.copy() def initialize_backward(self, ss): - if not all(k in ss for k in self.back_iter_vars): + if not all(k in ss for k in self.backward): ss.update(self.backward_init(ss)) def make_exog_law_of_motion(self, d:dict): diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py index 74f1cf4..24970bb 100644 --- a/src/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -134,9 +134,9 @@ def fiscal(r, B): @simple -def mkt_clearing(A, N_e, C, L, Y, B, pi, mu, kappa): +def mkt_clearing(A, N_E, C, L, Y, B, pi, mu, kappa): asset_mkt = A - B - labor_mkt = N_e - L + labor_mkt = N_E - L goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y return asset_mkt, labor_mkt, goods_mkt diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 63cdf83..0163531 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -275,10 +275,10 @@ def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): @simple -def mkt_clearing(p, A, B, Bg, C, I, G, Chi, psip, omega, Y): +def mkt_clearing(p, A, B, Bg, C, I, G, CHI, psip, omega, Y): wealth = A + B asset_mkt = p + Bg - wealth - goods_mkt = C + I + G + Chi + psip + omega * B - Y + goods_mkt = C + I + G + CHI + psip + omega * B - Y return asset_mkt, wealth, goods_mkt diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 28997bb..5c91f21 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -64,11 +64,11 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): household = hank_model['household'] T = 40 exogenous = ['r'] - output_list = household.non_back_iter_outputs + output_list = household.non_backward_outputs h = 1E-4 Js = household.jacobian(ss, exogenous, T=T) - Js_direct = {o.capitalize(): {i: np.empty((T, T)) for i in exogenous} for o in output_list} + Js_direct = {o.upper(): {i: np.empty((T, T)) for i in exogenous} for o in output_list} # run td once without any shocks to get paths to subtract against # (better than subtracting by ss since ss not exact) @@ -83,6 +83,6 @@ def test_fake_news_v_direct_method(one_asset_hank_dag): # store results as column t of J[o][i] for each outcome o for o in output_list: - Js_direct[o.capitalize()][i][:, t] = (td_out[o.capitalize()] - td_noshock[o.capitalize()]) / h + Js_direct[o.upper()][i][:, t] = (td_out[o.upper()] - td_noshock[o.upper()]) / h assert np.linalg.norm(Js['C']['r'] - Js_direct['C']['r'], np.inf) < 3e-4 From 58e0f5aadc87e2700f79e7675f5432171af74a46 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 12:01:00 -0500 Subject: [PATCH 261/288] fixed het decorator to have hetinputs and hetoutputs --- src/sequence_jacobian/blocks/het_block.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 45b62ac..86f285b 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -14,9 +14,9 @@ from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition, Transition -def het(exogenous, policy, backward, backward_init=None): +def het(exogenous, policy, backward, backward_init=None, hetinputs=None, hetoutputs=None): def decorator(backward_fun): - return HetBlock(backward_fun, exogenous, policy, backward, backward_init=backward_init) + return HetBlock(backward_fun, exogenous, policy, backward, backward_init, hetinputs, hetoutputs) return decorator From 0c45a34273fd36128290d74d1cd755ef86a8aa43 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 13:19:45 -0500 Subject: [PATCH 262/288] epic reorganization of repository --- src/sequence_jacobian/__init__.py | 10 +- .../blocks/adhoc_test_parent_block.py | 27 - .../auxiliary_blocks/jacobiandict_block.py | 7 +- .../{primitives.py => blocks/block.py} | 68 +- .../blocks/combined_block.py | 11 +- src/sequence_jacobian/blocks/discont_block.py | 903 ------------------ src/sequence_jacobian/blocks/het_block.py | 11 +- src/sequence_jacobian/blocks/simple_block.py | 9 +- src/sequence_jacobian/blocks/solved_block.py | 12 +- .../blocks/{ => support}/parent.py | 0 .../support/steady_state.py} | 6 +- .../impulse.py => classes/impulse_dict.py} | 7 +- .../classes.py => classes/jacobian_dict.py} | 282 +----- .../classes/sparse_jacobians.py | 337 +++++++ .../steady_state_dict.py} | 4 +- src/sequence_jacobian/devtools/__init__.py | 3 - src/sequence_jacobian/devtools/analysis.py | 242 ----- src/sequence_jacobian/devtools/debug.py | 106 -- src/sequence_jacobian/devtools/deprecate.py | 22 - src/sequence_jacobian/jacobian/__init__.py | 1 - src/sequence_jacobian/jacobian/support.py | 100 -- src/sequence_jacobian/models/hank.py | 4 +- src/sequence_jacobian/models/krusell_smith.py | 2 +- src/sequence_jacobian/models/two_asset.py | 2 +- .../steady_state/__init__.py | 1 - .../support => utilities}/bijection.py | 2 +- .../visualization/__init__.py | 1 - .../visualization/draw_dag.py | 161 ---- tests/base/test_jacobian.py | 2 +- tests/base/test_jacobian_dict_block.py | 2 +- tests/base/test_multiexog.py | 1 - tests/base/test_public_classes.py | 7 +- tests/base/test_simple_block.py | 2 +- tests/base/test_solved_block.py | 4 +- tests/base/test_workflow.py | 16 +- 35 files changed, 412 insertions(+), 1963 deletions(-) delete mode 100644 src/sequence_jacobian/blocks/adhoc_test_parent_block.py rename src/sequence_jacobian/{primitives.py => blocks/block.py} (88%) delete mode 100644 src/sequence_jacobian/blocks/discont_block.py rename src/sequence_jacobian/blocks/{ => support}/parent.py (100%) rename src/sequence_jacobian/{steady_state/support.py => blocks/support/steady_state.py} (99%) rename src/sequence_jacobian/{blocks/support/impulse.py => classes/impulse_dict.py} (97%) rename src/sequence_jacobian/{jacobian/classes.py => classes/jacobian_dict.py} (58%) create mode 100644 src/sequence_jacobian/classes/sparse_jacobians.py rename src/sequence_jacobian/{steady_state/classes.py => classes/steady_state_dict.py} (95%) delete mode 100644 src/sequence_jacobian/devtools/__init__.py delete mode 100644 src/sequence_jacobian/devtools/analysis.py delete mode 100644 src/sequence_jacobian/devtools/debug.py delete mode 100644 src/sequence_jacobian/devtools/deprecate.py delete mode 100644 src/sequence_jacobian/jacobian/__init__.py delete mode 100644 src/sequence_jacobian/jacobian/support.py delete mode 100644 src/sequence_jacobian/steady_state/__init__.py rename src/sequence_jacobian/{blocks/support => utilities}/bijection.py (97%) delete mode 100644 src/sequence_jacobian/visualization/__init__.py delete mode 100644 src/sequence_jacobian/visualization/draw_dag.py diff --git a/src/sequence_jacobian/__init__.py b/src/sequence_jacobian/__init__.py index 1dc0ac5..2e14daf 100644 --- a/src/sequence_jacobian/__init__.py +++ b/src/sequence_jacobian/__init__.py @@ -1,17 +1,15 @@ """Public-facing objects.""" -from . import estimation, jacobian, utilities, devtools +from . import estimation, utilities from .blocks.simple_block import simple from .blocks.het_block import het from .blocks.solved_block import solved from .blocks.combined_block import combine, create_model from .blocks.support.simple_displacement import apply_function -from .steady_state.classes import SteadyStateDict -from .jacobian.classes import JacobianDict -from .blocks.support.impulse import ImpulseDict - -from .visualization.draw_dag import draw_dag, draw_solved, inspect_solved +from .classes.steady_state_dict import SteadyStateDict +from .classes.impulse_dict import ImpulseDict +from .classes.jacobian_dict import JacobianDict # Useful utilities for setting up HetBlocks from .utilities.discretize import agrid, markov_rouwenhorst, markov_tauchen diff --git a/src/sequence_jacobian/blocks/adhoc_test_parent_block.py b/src/sequence_jacobian/blocks/adhoc_test_parent_block.py deleted file mode 100644 index 3bb06f4..0000000 --- a/src/sequence_jacobian/blocks/adhoc_test_parent_block.py +++ /dev/null @@ -1,27 +0,0 @@ -from parent import Parent -from sequence_jacobian.blocks.support.bijection import Bijection - -class DummyBlock: - def __init__(self, name): - self.name = name - -def test(): - grand1 = DummyBlock('grandkid1') - grand2 = DummyBlock('grandkid2') - grand2.unknowns = {'thing1': 3, 'thing2': 5} - grand2.M = Bijection({'thing1': 'othername1'}) - - kid2 = Parent([grand1, grand2], name='kid2') - kid2.M = Bijection({'thing2': 'othername2', 'othername1': 'othername3'}) - - b = Parent([DummyBlock('kid1'), - kid2, - DummyBlock('kid3')], name='me') - - b1 = b['grandkid1'] - assert isinstance(b1, DummyBlock) and b1.name == 'grandkid1' - assert b.path('grandkid2') == ['me', 'kid2', 'grandkid2'] - - assert b.get_attribute('grandkid2', 'unknowns') == {'othername3': 3, 'othername2': 5} - -test() diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index 735105b..b361c94 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -1,9 +1,8 @@ """A simple wrapper for JacobianDicts to be embedded in DAGs""" -from ...primitives import Block, Array -from ...jacobian.classes import JacobianDict -from ..support.impulse import ImpulseDict - +from ..block import Block +from ...classes.impulse_dict import ImpulseDict +from ...classes.jacobian_dict import JacobianDict class JacobianDictBlock(JacobianDict, Block): """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" diff --git a/src/sequence_jacobian/primitives.py b/src/sequence_jacobian/blocks/block.py similarity index 88% rename from src/sequence_jacobian/primitives.py rename to src/sequence_jacobian/blocks/block.py index 08403a3..9e42bdb 100644 --- a/src/sequence_jacobian/primitives.py +++ b/src/sequence_jacobian/blocks/block.py @@ -1,75 +1,31 @@ """Primitives to provide clarity and structure on blocks/models work""" -import abc import numpy as np -from abc import ABCMeta as NativeABCMeta from numbers import Real from typing import Any, Dict, Union, Tuple, Optional, List from copy import deepcopy -from sequence_jacobian.utilities.ordered_set import OrderedSet +from ..classes.steady_state_dict import SteadyStateDict, UserProvidedSS +from ..classes.impulse_dict import ImpulseDict +from ..classes.jacobian_dict import JacobianDict, FactoredJacobianDict +from .support.steady_state import provide_solver_default, solve_for_unknowns, compute_target_values +from .support.parent import Parent +from ..utilities import misc +from ..utilities.bijection import Bijection +from ..utilities.ordered_set import OrderedSet -from .steady_state.support import provide_solver_default, solve_for_unknowns, compute_target_values -from .steady_state.classes import SteadyStateDict, UserProvidedSS -from .jacobian.classes import JacobianDict, FactoredJacobianDict -from .blocks.support.impulse import ImpulseDict -from .blocks.support.bijection import Bijection -from .blocks.parent import Parent -from .utilities import misc - -# Basic types Array = Any - -############################################################################### -# Because abc doesn't implement "abstract attribute"s -# https://stackoverflow.com/questions/23831510/abstract-attribute-not-property -class DummyAttribute: - pass - - -def abstract_attribute(obj=None): - if obj is None: - obj = DummyAttribute() - obj.__is_abstract_attribute__ = True - return obj - - -class ABCMeta(NativeABCMeta): - - def __call__(cls, *args, **kwargs): - instance = NativeABCMeta.__call__(cls, *args, **kwargs) - abstract_attributes = { - name - for name in dir(instance) - if getattr(getattr(instance, name), '__is_abstract_attribute__', False) - } - if abstract_attributes: - raise NotImplementedError( - "Cannot instantiate abstract class `{}` with" - " abstract attributes: `{}`.\n" - "Define concrete implementations of these attributes in the child class prior to preceding.".format( - cls.__name__, - '`, `'.join(abstract_attributes) - ) - ) - return instance -############################################################################### - - -class Block(abc.ABC, metaclass=ABCMeta): +class Block: """The abstract base class for all `Block` objects.""" - #@abc.abstractmethod def __init__(self): self.M = Bijection({}) self.ss_valid_input_kwargs = misc.input_kwarg_list(self._steady_state) - @abstract_attribute def inputs(self): pass - @abstract_attribute def outputs(self): pass @@ -176,7 +132,7 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" " in the `helper_targets` keyword argument.") else: - from .steady_state.support import augment_dag_w_helper_blocks + from .support.steady_state import augment_dag_w_helper_blocks dag, ss, unknowns_to_solve, targets_to_solve = augment_dag_w_helper_blocks(self, calibration, unknowns, targets, helper_blocks, helper_targets) @@ -277,14 +233,14 @@ def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, inputs, T) - from . import combine + from sequence_jacobian import combine self_with_unknowns = combine([U_Z, self]) return self_with_unknowns.jacobian(ss, inputs, unknowns | outputs, T, Js) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: name = self.name + "_solved" - from .blocks.solved_block import SolvedBlock + from .solved_block import SolvedBlock return SolvedBlock(self, name, unknowns, targets, solver, solver_kwargs) def remap(self, map): diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 2f4bdb8..cc193fa 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -2,14 +2,11 @@ from copy import deepcopy -from ..primitives import Block -from ..utilities.misc import dict_diff +from .block import Block from ..utilities.graph import block_sort, find_intermediate_inputs -from ..utilities.graph import topological_sort -from ..utilities.ordered_set import OrderedSet -from ..blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock -from ..blocks.parent import Parent -from ..jacobian.classes import JacobianDict +from .auxiliary_blocks.jacobiandict_block import JacobianDictBlock +from .support.parent import Parent +from ..classes.jacobian_dict import JacobianDict def combine(blocks, name="", model_alias=False): diff --git a/src/sequence_jacobian/blocks/discont_block.py b/src/sequence_jacobian/blocks/discont_block.py deleted file mode 100644 index 3ea0d3b..0000000 --- a/src/sequence_jacobian/blocks/discont_block.py +++ /dev/null @@ -1,903 +0,0 @@ -import copy -import numpy as np - -from .support.impulse import ImpulseDict -from ..primitives import Block -from .. import utilities as utils -from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict -from ..utilities.misc import verify_saved_jacobian -from .support.bijection import Bijection - - -def discont(exogenous, policy, disc_policy, backward, backward_init=None): - def decorator(back_step_fun): - return DiscontBlock(back_step_fun, exogenous, policy, disc_policy, backward, backward_init=backward_init) - return decorator - - -class DiscontBlock(Block): - """Part 1: Initializer for DiscontBlock, intended to be called via @hetdc() decorator on backward step function. - - IMPORTANT: All `policy` and non-aggregate output variables of this HetBlock need to be *lower-case*, since - the methods that compute steady state, transitional dynamics, and Jacobians for HetBlocks automatically handle - aggregation of non-aggregate outputs across the distribution and return aggregates as upper-case equivalents - of the `policy` and non-aggregate output variables specified in the backward step function. - """ - - def __init__(self, back_step_fun, exogenous, policy, disc_policy, backward, backward_init=None): - """Construct HetBlock from backward iteration function. - - Parameters - ---------- - back_step_fun : function - backward iteration function - exogenous : str - name of Markov transition matrix for exogenous variable - (now only single allowed for simplicity; use Kronecker product for more) - policy : str or sequence of str - names of policy variables of endogenous, continuous state variables - e.g. assets 'a', must be returned by function - disc_policy: str - name of policy function for discrete choices (probabilities) - backward : str or sequence of str - variables that together comprise the 'v' that we use for iterating backward - must appear both as outputs and as arguments - - It is assumed that every output of the function (except possibly backward), including policy, - will be on a grid of dimension 1 + len(policy), where the first dimension is the exogenous - variable and then the remaining dimensions are each of the continuous policy variables, in the - same order they are listed in 'policy'. - - The Markov transition matrix between the current and future period and backward iteration - variables should appear in the backward iteration function with '_p' subscripts ("prime") to - indicate that they come from the next period. - - Currently, we only support up to two policy variables. - """ - self.name = back_step_fun.__name__ - super().__init__() - - # self.back_step_fun is one iteration of the backward step function pertaining to a given HetBlock. - # i.e. the function pertaining to equation (14) in the paper: v_t = curlyV(v_{t+1}, X_t) - self.back_step_fun = back_step_fun - - # self.back_step_outputs and self.back_step_inputs are all of the output and input arguments of - # self.back_step_fun, the variables used in the backward iteration, - # which generally include value and/or policy functions. - self.back_step_output_list = utils.misc.output_list(back_step_fun) - self.back_step_outputs = set(self.back_step_output_list) - self.back_step_inputs = set(utils.misc.input_list(back_step_fun)) - - # See the docstring of HetBlock for details on the attributes directly below - self.disc_policy = disc_policy - self.policy = policy - self.exogenous, self.back_iter_vars = (utils.misc.make_tuple(x) for x in (exogenous, backward)) - - # self.inputs_to_be_primed indicates all variables that enter into self.back_step_fun whose name has "_p" - # (read as prime). Because it's the case that the initial dict of input arguments for self.back_step_fun - # contains the names of these variables that omit the "_p", we need to swap the key from the unprimed to - # the primed key name, such that self.back_step_fun will properly call those variables. - # e.g. the key "Va" will become "Va_p", associated to the same value. - self.inputs_to_be_primed = set(self.exogenous) | set(self.back_iter_vars) - - # self.non_back_iter_outputs are all of the outputs from self.back_step_fun excluding the backward - # iteration variables themselves. - self.non_back_iter_outputs = self.back_step_outputs - set(self.back_iter_vars) - set(self.disc_policy) - - # self.outputs and self.inputs are the *aggregate* outputs and inputs of this HetBlock, which are used - # in utils.graph.block_sort to topologically sort blocks along the DAG - # according to their aggregate outputs and inputs. - self.outputs = {o.capitalize() for o in self.non_back_iter_outputs} - self.inputs = self.back_step_inputs - {k + '_p' for k in self.back_iter_vars} - for ex in self.exogenous: - self.inputs.remove(ex + '_p') - self.inputs.add(ex) - - # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. - # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. - self.hetinput = None - self.hetinput_inputs = set() - self.hetinput_outputs = set() - self.hetinput_outputs_order = tuple() - - # start without a hetoutput - self.hetoutput = None - self.hetoutput_inputs = set() - self.hetoutput_outputs = set() - self.hetoutput_outputs_order = tuple() - - # The set of variables that will be wrapped in a separate namespace for this HetBlock - # as opposed to being available at the top level - self.internal = utils.misc.smart_set(self.back_step_outputs) | utils.misc.smart_set(self.exogenous) | {"D"} - - if len(self.policy) > 1: - raise ValueError(f"More than one continuous states in {back_step_fun.__name__}, not yet supported") - - # Checking that the various inputs/outputs attributes are correctly set - for ex in self.exogenous: - if ex + '_p' not in self.back_step_inputs: - raise ValueError(f"Markov matrix '{ex}_p' not included as argument in {back_step_fun.__name__}") - - if self.policy not in self.back_step_outputs: - raise ValueError(f"Policy '{self.policy}' not included as output in {back_step_fun.__name__}") - if self.policy[0].isupper(): - raise ValueError(f"Policy '{self.policy}' is uppercase in {back_step_fun.__name__}, which is not allowed") - - for back in self.back_iter_vars: - if back + '_p' not in self.back_step_inputs: - raise ValueError(f"Backward variable '{back}_p' not included as argument in {back_step_fun.__name__}") - - if back not in self.back_step_outputs: - raise ValueError(f"Backward variable '{back}' not included as output in {back_step_fun.__name__}") - - for out in self.non_back_iter_outputs: - if out[0].isupper(): - raise ValueError("Output '{out}' is uppercase in {back_step_fun.__name__}, which is not allowed") - - # Add the backward iteration initializer function (the initial guesses for self.back_iter_vars) - if backward_init is None: - # TODO: Think about implementing some "automated way" of providing - # an initial guess for the backward iteration. - self.backward_init = backward_init - else: - self.backward_init = backward_init - - # note: should do more input checking to ensure certain choices not made: 'D' not input, etc. - - def __repr__(self): - """Nice string representation of HetBlock for printing to console""" - if self.hetinput is not None: - if self.hetoutput is not None: - return f"" - else: - return f"" - else: - return f"" - - '''Part 2: high-level routines, with first three called analogously to SimpleBlock counterparts - - steady_state : do backward and forward iteration until convergence to get complete steady state - - impulse_nonlinear : do backward and forward iteration up to T to compute dynamics given some shocks - - impulse_linear : apply jacobians to compute linearized dynamics given some shocks - - jacobian : compute jacobians of outputs with respect to shocked inputs, using fake news algorithm - - - add_hetinput : add a hetinput to the HetBlock that first processes inputs through function hetinput - - add_hetoutput: add a hetoutput to the HetBlock that is computed after the entire ss computation, or after - each backward iteration step in td, jacobian is not computed for these! - ''' - - - def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, - forward_tol=1E-10, forward_maxit=100_000, hetoutput=False): - """Evaluate steady state HetBlock using keyword args for all inputs. Analog to SimpleBlock.ss. - - Parameters - ---------- - backward_tol : [optional] float - in backward iteration, max abs diff between policy in consecutive steps needed for convergence - backward_maxit : [optional] int - maximum number of backward iterations, if 'backward_tol' not reached by then, raise error - forward_tol : [optional] float - in forward iteration, max abs diff between dist in consecutive steps needed for convergence - forward_maxit : [optional] int - maximum number of forward iterations, if 'forward_tol' not reached by then, raise error - - kwargs : dict - The following inputs are required as keyword arguments, which show up in 'kwargs': - - The exogenous Markov matrix, e.g. Pi=... if self.exogenous=='Pi' - - A seed for each backward variable, e.g. Va=... and Vb=... if self.back_iter_vars==('Va','Vb') - - A grid for each policy variable, e.g. a_grid=... and b_grid=... if self.policy==('a','b') - - All other inputs to the backward iteration function self.back_step_fun, except _p added to - for self.exogenous and self.back_iter_vars, for which the method uses steady-state values. - If there is a self.hetinput, then we need the inputs to that, not to self.back_step_fun. - - Other inputs in 'kwargs' are optional: - - A seed for the distribution: D=... - - If no seed for the distribution is provided, a seed for the invariant distribution - of the Markov process, e.g. Pi_seed=... if self.exogenous=='Pi' - - Returns - ---------- - ss : dict, contains - - ss inputs of self.back_step_fun and (if present) self.hetinput - - ss outputs of self.back_step_fun - - ss distribution 'D' - - ss aggregates (in uppercase) for all outputs of self.back_step_fun except self.back_iter_vars - """ - - ss = copy.deepcopy(calibration.toplevel) - - # extract information from calibration - grid = calibration[self.policy + '_grid'] - D_seed = calibration.get('D', None) - - # run backward iteration - sspol = self.policy_ss(calibration, tol=backward_tol, maxit=backward_maxit) - ss.update(sspol) - - # run forward iteration - D = self.dist_ss(sspol, grid, forward_tol, forward_maxit, D_seed) - ss.update({"D": D}) - - # aggregate all outputs other than backward variables on grid, capitalize - aggregates = {o.capitalize(): np.vdot(D, sspol[o]) for o in self.non_back_iter_outputs} - ss.update(aggregates) - - if hetoutput and self.hetoutput is not None: - hetoutputs = self.hetoutput.evaluate(ss) - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutputs, D, ss, mode="ss") - else: - hetoutputs = {} - aggregate_hetoutputs = {} - ss.update({**hetoutputs, **aggregate_hetoutputs}) - - return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, - {self.name: {k: ss[k] for k in ss if k in self.internal}}) - - def _impulse_nonlinear(self, ss, inputs, outputs, Js, returnindividual=False, grid_paths=None): - """Evaluate transitional dynamics for DiscontBlock given dynamic paths for inputs in exogenous, - assuming that we start and end in steady state ss, and that all inputs not specified in - exogenous are constant at their ss values. Analog to SimpleBlock.td. - - Block-specific inputs - --------------------- - returnindividual : [optional] bool - return distribution and full outputs on grid - grid_paths: [optional] dict of {str: array(T, Number of grid points)} - time-varying grids for policies - - Returns - ---------- - td : dict - if returnindividual = False, time paths for aggregates (uppercase) for all outputs - of self.back_step_fun except self.back_iter_vars - if returnindividual = True, additionally time paths for distribution and for all outputs - of self.back_Step_fun on the full grid - """ - T = inputs.T - D = ss.internal[self.name]['D'] - P = ss.internal[self.name][self.disc_policy] - - # construct grids for policy variables either from the steady state grid if the grid is meant to be - # non-time-varying or from the provided `grid_path` if the grid is meant to be time-varying. - if grid_paths is not None and self.policy in grid_paths: - grid = grid_paths[self.policy] - use_ss_grid = False - else: - grid = ss[self.policy + "_grid"] - use_ss_grid = True - # sspol_i, sspol_pi = utils.interpolate_coord_robust(grid, ss[self.policy]) - - # allocate empty arrays to store result, assume all like D - individual_paths = {k: np.empty((T,) + D.shape) for k in self.non_back_iter_outputs} - hetoutput_paths = {k: np.empty((T,) + D.shape) for k in self.hetoutput_outputs} - P_path = np.empty((T,) + P.shape) - - # obtain full path of multidimensional inputs - multidim_inputs = {k: np.empty((T,) + ss.internal[self.name][k].shape) for k in self.hetinput_outputs_order} - if self.hetinput is not None: - indict = dict(ss.items()) - for t in range(T): - indict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) - hetout = dict(zip(self.hetinput_outputs_order, - self.hetinput(**{k: indict[k] for k in self.hetinput_inputs}))) - for k in self.hetinput_outputs_order: - multidim_inputs[k][t, ...] = hetout[k] - - # backward iteration - backdict = dict(ss.items()) - backdict.update(copy.deepcopy(ss.internal[self.name])) - for t in reversed(range(T)): - # be careful: if you include vars from self.back_iter_vars in exogenous, agents will use them! - backdict.update({k: ss[k] + v[t, ...] for k, v in inputs.items()}) - - # add in multidimensional inputs EXCEPT exogenous state transitions (at lead 0) - backdict.update({k: ss.internal[self.name][k] + v[t, ...] for k, v in multidim_inputs.items() if k not in self.exogenous}) - - # add in multidimensional inputs FOR exogenous state transitions (at lead 1) - if t < T - 1: - backdict.update({k: ss.internal[self.name][k] + v[t+1, ...] for k, v in multidim_inputs.items() if k in self.exogenous}) - - # step back - individual = {k: v for k, v in zip(self.back_step_output_list, - self.back_step_fun(**self.make_inputs(backdict)))} - - # update backward variables - backdict.update({k: individual[k] for k in self.back_iter_vars}) - - # compute hetoutputs - if self.hetoutput is not None: - hetoutput = self.hetoutput.evaluate(backdict) - for k in self.hetoutput_outputs: - hetoutput_paths[k][t, ...] = hetoutput[k] - - # save individual outputs of interest - P_path[t, ...] = individual[self.disc_policy] - for k in self.non_back_iter_outputs: - individual_paths[k][t, ...] = individual[k] - - # forward iteration - # initial markov matrix (may have been shocked) - Pi_path = [[multidim_inputs[k][0, ...] if k in self.hetinput_outputs_order else ss[k] for k in self.exogenous]] - - # on impact: assets are predetermined, but Pi could be shocked, and P can change - D_path = np.empty((T,) + D.shape) - if use_ss_grid: - grid_var = grid - else: - grid_var = grid[0, ...] - sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid_var, ss.internal[self.name][self.policy]) - D_path[0, ...] = self.forward_step(D, P_path[0, ...], Pi_path[0], sspol_i, sspol_pi) - for t in range(T-1): - # have to interpolate policy separately for each t to get sparse transition matrices - if not use_ss_grid: - grid_var = grid[t, ...] - pol_i, pol_pi = utils.interpolate.interpolate_coord_robust(grid_var, individual_paths[self.policy][t, ...]) - - # update exogenous Markov matrices - Pi = [multidim_inputs[k][t+1, ...] if k in self.hetinput_outputs_order else ss[k] for k in self.exogenous] - Pi_path.append(Pi) - - # step forward - D_path[t+1, ...] = self.forward_step(D_path[t, ...], P_path[t+1, ...], Pi, pol_i, pol_pi) - - # obtain aggregates of all outputs, made uppercase - aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) - for o in self.non_back_iter_outputs} - if self.hetoutput: - aggregate_hetoutputs = self.hetoutput.aggregate(hetoutput_paths, D_path, backdict, mode="td") - else: - aggregate_hetoutputs = {} - - # return either this, or also include distributional information - if returnindividual: - return ImpulseDict({**aggregates, **aggregate_hetoutputs, **individual_paths, **multidim_inputs, - **hetoutput_paths, 'D': D_path, 'P_path': P_path, 'Pi_path': Pi_path}) - ss - else: - return ImpulseDict({**aggregates, **aggregate_hetoutputs}) - ss - - def _impulse_linear(self, ss, inputs, outputs, Js): - return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) - - def _jacobian(self, ss, inputs, outputs, T, h=1E-4): - """ - Block-specific inputs - --------------------- - h : [optional] float - h for numerical differentiation of backward iteration - """ - outputs = self.M_outputs.inv @ outputs # horrible - relevant_shocks = [i for i in self.back_step_inputs | self.hetinput_inputs if i in inputs] - - # step 0: preliminary processing of steady state - (ssin_dict, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space, D0, D2, Pi, P) = self.jac_prelim(ss) - - # step 1 of fake news algorithm - # compute curlyY and curlyD (backward iteration) for each input i - dYs, dDs, dD_ps, dD_direct = {}, {}, {}, {} - for i in relevant_shocks: - dYs[i], dDs[i], dD_ps[i], dD_direct[i] = self.backward_iteration_fakenews(i, outputs, ssin_dict, ssout_list, - ss.internal[self.name]['D'], - D0, D2, P, Pi, sspol_i, sspol_pi, - sspol_space, T, h, - ss_for_hetinput) - - # step 2 of fake news algorithm - # compute prediction vectors curlyP (forward iteration) for each outcome o - curlyPs = {} - for o in outputs: - curlyPs[o] = self.forward_iteration_fakenews(ss.internal[self.name][o], Pi, P, sspol_i, sspol_pi, T) - - # steps 3-4 of fake news algorithm - # make fake news matrix and Jacobian for each outcome-input pair - F, J = {}, {} - for o in outputs: - for i in relevant_shocks: - if o.capitalize() not in F: - F[o.capitalize()] = {} - if o.capitalize() not in J: - J[o.capitalize()] = {} - F[o.capitalize()][i] = DiscontBlock.build_F(dYs[i][o], dD_ps[i], curlyPs[o], dD_direct[i], dDs[i]) - J[o.capitalize()][i] = DiscontBlock.J_from_F(F[o.capitalize()][i]) - - return JacobianDict(J, name=self.name) - - def add_hetinput(self, hetinput, overwrite=False, verbose=True): - """Add a hetinput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetinput function. - - A `hetinput` is any non-scalar-valued input argument provided to the HetBlock's backward iteration function, - self.back_step_fun, which is of the same dimensions as the distribution of agents in the HetBlock over - the relevant idiosyncratic state variables, generally referred to as `D`. e.g. The one asset HANK model - example provided in the models directory of sequence_jacobian has a hetinput `T`, which is skill-specific - transfers. - """ - if self.hetinput is not None and overwrite is False: - raise ValueError('Trying to attach hetinput when one already exists!') - else: - if verbose: - if self.hetinput is not None and overwrite is True: - print(f"Overwriting current hetinput, {self.hetinput.__name__} with new hetinput," - f" {hetinput.__name__}!") - else: - print(f"Added hetinput {hetinput.__name__} to the {self.back_step_fun.__name__} HetBlock") - - self.hetinput = hetinput - self.hetinput_inputs = set(utils.misc.input_list(hetinput)) - self.hetinput_outputs = set(utils.misc.output_list(hetinput)) - self.hetinput_outputs_order = utils.misc.output_list(hetinput) - - # modify inputs to include hetinput's additional inputs, remove outputs - self.inputs |= self.hetinput_inputs - self.inputs -= self.hetinput_outputs - - self.internal |= self.hetinput_outputs - - def add_hetoutput(self, hetoutput, overwrite=False, verbose=True): - """Add a hetoutput to this HetBlock. Any call to self.back_step_fun will first process - inputs through the hetoutput function. - - A `hetoutput` is any *non-scalar-value* output that the user might desire to be calculated from - the output arguments of the HetBlock's backward iteration function. Importantly, as of now the `hetoutput` - cannot be a function of time displaced values of the HetBlock's outputs but rather must be able to - be calculated from the outputs statically. e.g. The two asset HANK model example provided in the models - directory of sequence_jacobian has a hetoutput, `chi`, the adjustment costs for any initial level of assets - `a`, to any new level of assets `a'`. - """ - if self.hetoutput is not None and overwrite is False: - raise ValueError('Trying to attach hetoutput when one already exists!') - else: - if verbose: - if self.hetoutput is not None and overwrite is True: - print(f"Overwriting current hetoutput, {self.hetoutput.name} with new hetoutput," - f" {hetoutput.name}!") - else: - print(f"Added hetoutput {hetoutput.name} to the {self.back_step_fun.__name__} HetBlock") - - self.hetoutput = hetoutput - self.hetoutput_inputs = set(hetoutput.input_list) - self.hetoutput_outputs = set(hetoutput.output_list) - self.hetoutput_outputs_order = hetoutput.output_list - - # Modify the HetBlock's inputs to include additional inputs required for computing both the hetoutput - # and aggregating the hetoutput, but do not include: - # 1) objects computed within the HetBlock's backward iteration that enter into the hetoutput computation - # 2) objects computed within hetoutput that enter into hetoutput's aggregation (self.hetoutput.outputs) - # 3) D, the cross-sectional distribution of agents, which is used in the hetoutput aggregation - # but is computed after the backward iteration - self.inputs |= (self.hetoutput_inputs - self.hetinput_outputs - self.back_step_outputs - self.hetoutput_outputs - set("D")) - # Modify the HetBlock's outputs to include the aggregated hetoutputs - self.outputs |= set([o.capitalize() for o in self.hetoutput_outputs]) - - self.internal |= self.hetoutput_outputs - - '''Part 3: components of ss(): - - policy_ss : backward iteration to get steady-state policies and other outcomes - - dist_ss : forward iteration to get steady-state distribution and compute aggregates - ''' - - def policy_ss(self, ssin, tol=1E-8, maxit=5000): - """Find steady-state policies and backward variables through backward iteration until convergence. - - Parameters - ---------- - ssin : dict - all steady-state inputs to back_step_fun, including seed values for backward variables - tol : [optional] float - max diff between consecutive iterations of policy variables needed for convergence - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - - Returns - ---------- - sspol : dict - all steady-state outputs of backward iteration, combined with inputs to backward iteration - """ - - # find initial values for backward iteration and account for hetinputs - original_ssin = ssin - ssin = self.make_inputs(ssin) - - old = {} - for it in range(maxit): - try: - # run and store results of backward iteration, which come as tuple, in dict - sspol = {k: v for k, v in zip(self.back_step_output_list, self.back_step_fun(**ssin))} - except KeyError as e: - print(f'Missing input {e} to {self.back_step_fun.__name__}!') - raise - - # only check convergence every 10 iterations for efficiency - if it % 10 == 1 and all(utils.optimized_routines.within_tolerance(sspol[k], old[k], tol) - for k in self.back_iter_vars): - break - - # update 'old' for comparison during next iteration, prepare 'ssin' as input for next iteration - old.update({k: sspol[k] for k in self.back_iter_vars}) - ssin.update({k + '_p': sspol[k] for k in self.back_iter_vars}) - else: - raise ValueError(f'No convergence of policy functions after {maxit} backward iterations!') - - # want to record inputs in ssin, but remove _p, add in hetinput inputs if there - for k in self.inputs_to_be_primed: - ssin[k] = ssin[k + '_p'] - del ssin[k + '_p'] - if self.hetinput is not None: - for k in self.hetinput_inputs: - if k in original_ssin: - ssin[k] = original_ssin[k] - return {**ssin, **sspol} - - def dist_ss(self, sspol, grid, tol=1E-10, maxit=100_000, D_seed=None): - """Find steady-state distribution through forward iteration until convergence. - - Parameters - ---------- - sspol : dict - steady-state policies on grid for all policy variables in self.policy - grid : dict - grids for all policy variables in self.policy - tol : [optional] float - absolute tolerance for max diff between consecutive iterations for distribution - maxit : [optional] int - maximum number of iterations, if 'tol' not reached by then, raise error - D_seed : [optional] array - initial seed for overall distribution - - Returns - ---------- - D : array - steady-state distribution - """ - # extract transition matrix for exogenous states - Pi = [sspol[k] for k in self.exogenous] - P = sspol[self.disc_policy] - - # first obtain initial distribution D - if D_seed is None: - # initialize at uniform distribution - D = np.ones_like(sspol[self.policy]) / sspol[self.policy].size - else: - D = D_seed - - # obtain interpolated policy rule for each dimension of endogenous policy - sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid, sspol[self.policy]) - - # iterate until convergence by tol, or maxit - for it in range(maxit): - Dnew = self.forward_step(D, P, Pi, sspol_i, sspol_pi) - - # only check convergence every 10 iterations for efficiency - if it % 10 == 0 and utils.optimized_routines.within_tolerance(D, Dnew, tol): - break - D = Dnew - else: - raise ValueError(f'No convergence after {maxit} forward iterations!') - - return D - - '''Part 4: components of jac(), corresponding to *4 steps of fake news algorithm* in paper - - Step 1: backward_step_fakenews and backward_iteration_fakenews to get curlyYs and curlyDs - - Step 2: forward_iteration_fakenews to get curlyPs - - Step 3: build_F to get fake news matrix from curlyYs, curlyDs, curlyPs - - Step 4: J_from_F to get Jacobian from fake news matrix - ''' - - def shock_timing(self, input_shocked, D0, Pi_ss, P, ss_for_hetinput, h): - """Figure out the details of how the scalar shock feeds into back_step_fun. - - Main complication: shocks to Pi transmit via hetinput with a lead of 1. - """ - if self.hetinput is not None and input_shocked in self.hetinput_inputs: - # if input_shocked is an input to hetinput, take numerical diff to get response - din_dict = dict(zip(self.hetinput_outputs_order, - utils.differentiate.numerical_diff_symmetric(self.hetinput, ss_for_hetinput, - {input_shocked: 1}, h))) - - if all(k not in din_dict.keys() for k in self.exogenous): - # if Pi is not generated by hetinput, no work to be done - lead = 0 - dD3_direct = None - elif all(np.count_nonzero(din_dict[k]) == 0 for k in self.exogenous if k in din_dict): - # if Pi is generated by hetinput but input_shocked does not affect it, replace Pi with Pi_p - lead = 0 - dD3_direct = None - for k in self.exogenous: - if k in din_dict.keys(): - din_dict[k + '_p'] = din_dict.pop(k) - else: - # if Pi is generated by hetinput and input_shocked affects it, replace that with Pi_p at lead 1 - lead = 1 - Pi = [din_dict[k] if k in din_dict else Pi_ss[num] for num, k in enumerate(self.exogenous)] - dD2_direct = utils.forward_step.forward_step_exo(D0, Pi) - dD3_direct = utils.forward_step.forward_step_dpol(dD2_direct, P) - for k in self.exogenous: - if k in din_dict.keys(): - din_dict[k + '_p'] = din_dict.pop(k) - else: - # if input_shocked feeds directly into back_step_fun with lead 0, no work to be done - lead = 0 - din_dict = {input_shocked: 1} - dD3_direct = None - - return din_dict, lead, dD3_direct - - def backward_step_fakenews(self, din_dict, output_list, ssin_dict, ssout_list, - Dss, D2, P, Pi, sspol_i, sspol_pi, sspol_space, h=1E-4): - # 1. shock perturbs outputs - shocked_outputs = {k: v for k, v in zip(self.back_step_output_list, - utils.differentiate.numerical_diff(self.back_step_fun, - ssin_dict, din_dict, h, - ssout_list))} - dV = {k: shocked_outputs[k] for k in self.back_iter_vars} - - # 2. which affects the distribution tomorrow via the savings policy - pol_pi_shock = -shocked_outputs[self.policy] / sspol_space - if "delev_exante" in din_dict: - # include an additional term to account for the effect of a deleveraging shock affecting the grid - dx = np.zeros_like(sspol_pi) - dx[sspol_i == 0] = 1. - add_term = sspol_pi * dx / sspol_space - pol_pi_shock += add_term - dD3_p = self.forward_step_shock(Dss, sspol_i, pol_pi_shock, Pi, P) - - # 3. and the distribution today (and Dmid tomorrow) via the discrete choice - P_shock = shocked_outputs[self.disc_policy] - dD3 = utils.forward_step.forward_step_dpol(D2, P_shock) # s[0], z[0], a[-1] - - # 4. and the aggregate outcomes today (ignoring dD and dD_direct) - dY = {k: np.vdot(Dss, shocked_outputs[k]) for k in output_list} - - return dV, dD3, dD3_p, dY - - def backward_iteration_fakenews(self, input_shocked, output_list, ssin_dict, ssout_list, Dss, D0, D2, P, Pi, - sspol_i, sspol_pi, sspol_space, T, h=1E-4, ss_for_hetinput=None): - """Iterate policy steps backward T times for a single shock.""" - # map name of shocked input into a perturbation of the inputs of back_step_fun - din_dict, lead, dD_direct = self.shock_timing(input_shocked, D0, Pi.copy(), P, ss_for_hetinput, h) - - # contemporaneous response to unit scalar shock - dV, dD, dD_p, dY = self.backward_step_fakenews(din_dict, output_list, ssin_dict, ssout_list, - Dss, D2, P, Pi, sspol_i, sspol_pi, sspol_space, h=h) - - # infer dimensions from this and initialize empty arrays - dDs = np.empty((T,) + dD.shape) - dD_ps = np.empty((T,) + dD_p.shape) - dYs = {k: np.empty(T) for k in dY.keys()} - - # fill in current effect of shock (be careful to handle lead = 1) - dDs[:lead, ...], dD_ps[:lead, ...] = 0, 0 - dDs[lead, ...], dD_ps[lead, ...] = dD, dD_p - for k in dY.keys(): - dYs[k][:lead] = 0 - dYs[k][lead] = dY[k] - - # fill in anticipation effects - for t in range(lead + 1, T): - dV, dDs[t, ...], dD_ps[t, ...], dY = self.backward_step_fakenews({k + '_p': - v for k, v in dV.items()}, - output_list, ssin_dict, ssout_list, - Dss, D2, P, Pi, sspol_i, sspol_pi, - sspol_space, h) - - for k in dY.keys(): - dYs[k][t] = dY[k] - - return dYs, dDs, dD_ps, dD_direct - - def forward_iteration_fakenews(self, o_ss, Pi, P, pol_i_ss, pol_pi_ss, T): - """Iterate transpose forward T steps to get full set of curlyPs for a given outcome. - - Note we depart from definition in paper by applying the demeaning operator in addition to Lambda - at each step. This does not affect products with curlyD (which are the only way curlyPs enter - Jacobian) since perturbations to distribution always have mean zero. It has numerical benefits - since curlyPs now go to zero for high t (used in paper in proof of Proposition 1). - """ - curlyPs = np.empty((T,) + o_ss.shape) - curlyPs[0, ...] = utils.misc.demean(o_ss) - for t in range(1, T): - curlyPs[t, ...] = utils.misc.demean(self.forward_step_transpose(curlyPs[t - 1, ...], - P, Pi, pol_i_ss, pol_pi_ss)) - return curlyPs - - @staticmethod - def build_F(dYs, dD_ps, curlyPs, dD_direct, dDs): - T = dYs.shape[0] - F = np.empty((T, T)) - - # standard effect - F[0, :] = dYs - F[1:, :] = curlyPs[:T-1, ...].reshape((T-1, -1)) @ dD_ps.reshape((T, -1)).T - - # contemporaneous effect via discrete choice - if dDs is not None: - F += curlyPs.reshape((T, -1)) @ dDs.reshape((T, -1)).T - - # direct effect of shock - if dD_direct is not None: - F[:, 0] += curlyPs.reshape((T, -1)) @ dD_direct.ravel() - - return F - - @staticmethod - def J_from_F(F): - J = F.copy() - for t in range(1, J.shape[1]): - J[1:, t] += J[:-1, t - 1] - return J - - '''Part 5: helpers for .jac: preliminary processing''' - - def jac_prelim(self, ss): - """Helper that does preliminary processing of steady state for fake news algorithm. - - Parameters - ---------- - ss : dict, all steady-state info, intended to be from .ss() - - Returns - ---------- - ssin_dict : dict, ss vals of exactly the inputs needed by self.back_step_fun for backward step - D0 : array (nS, nZ, nA), distribution over s[-1], z[-1], a[-1] - ssout_list : tuple, what self.back_step_fun returns when given ssin_dict (not exactly the same - as steady-state numerically since SS convergence was to some tolerance threshold) - ss_for_hetinput : dict, ss vals of exactly the inputs needed by self.hetinput (if it exists) - sspol_i : dict, indices on lower bracketing gridpoint for all in self.policy - sspol_pi : dict, weights on lower bracketing gridpoint for all in self.policy - sspol_space : dict, space between lower and upper bracketing gridpoints for all in self.policy - """ - # preliminary a: obtain ss inputs and other info, run once to get baseline for numerical differentiation - ssin_dict = self.make_inputs(ss) - ssout_list = self.back_step_fun(**ssin_dict) - - ss_for_hetinput = None - if self.hetinput is not None: - ss_for_hetinput = {k: ss[k] for k in self.hetinput_inputs if k in ss} - - # preliminary b: get sparse representations of policy rules and distance between neighboring policy gridpoints - grid = ss[self.policy + '_grid'] - sspol_i, sspol_pi = utils.interpolate.interpolate_coord_robust(grid, ss.internal[self.name][self.policy]) - sspol_space = grid[sspol_i + 1] - grid[sspol_i] - - # preliminary c: get end-of-period distribution, need it when Pi is shocked - Pi = [ss.internal[self.name][k] for k in self.exogenous] - D = ss.internal[self.name]['D'] - D0 = utils.forward_step.forward_step_cpol(D, sspol_i, sspol_pi) - D2 = utils.forward_step.forward_step_exo(D0, Pi) - - Pss = ss.internal[self.name][self.disc_policy] - - toreturn = (ssin_dict, ssout_list, ss_for_hetinput, sspol_i, sspol_pi, sspol_space, D0, D2, Pi, Pss) - - return toreturn - - '''Part 6: helper to extract inputs and potentially process them through hetinput''' - - def make_inputs(self, back_step_inputs_dict): - """Extract from back_step_inputs_dict exactly the inputs needed for self.back_step_fun, - process stuff through self.hetinput first if it's there. - """ - input_dict = copy.deepcopy(back_step_inputs_dict) - - # TODO: This make_inputs function needs to be revisited since it creates inputs both for initial steady - # state computation as well as for Jacobian/impulse evaluation for HetBlocks, - # where in the former the hetinputs and value function have yet to be computed, - # whereas in the latter they have already been computed - # and hence do not need to be recomputed. There may be room to clean this function up a bit. - if isinstance(back_step_inputs_dict, SteadyStateDict): - input_dict = copy.deepcopy(back_step_inputs_dict.toplevel) - input_dict.update({k: v for k, v in back_step_inputs_dict.internal[self.name].items()}) - else: - # If this HetBlock has a hetinput, then we need to compute the outputs of the hetinput first and include - # them as inputs for self.back_step_fun - if self.hetinput is not None: - outputs_as_tuple = utils.misc.make_tuple(self.hetinput(**{k: input_dict[k] - for k in self.hetinput_inputs if k in input_dict})) - input_dict.update(dict(zip(self.hetinput_outputs_order, outputs_as_tuple))) - - # Check if there are entries in indict corresponding to self.inputs_to_be_primed. - # In particular, we are interested in knowing if an initial value - # for the backward iteration variable has been provided. - # If it has not been provided, then use self.backward_init to calculate the initial values. - if not self.inputs_to_be_primed.issubset(set(input_dict.keys())): - initial_value_input_args = [input_dict[arg_name] for arg_name in utils.misc.input_list(self.backward_init)] - input_dict.update(zip(utils.misc.output_list(self.backward_init), - utils.misc.make_tuple(self.backward_init(*initial_value_input_args)))) - - for i_p in self.inputs_to_be_primed: - input_dict[i_p + "_p"] = input_dict[i_p] - del input_dict[i_p] - - try: - return {k: input_dict[k] for k in self.back_step_inputs if k in input_dict} - except KeyError as e: - print(f'Missing backward variable or Markov matrix {e} for {self.back_step_fun.__name__}!') - raise - - '''Part 7: routines to do forward steps of different kinds, all wrap functions in utils''' - - - def forward_step(self, D3_prev, P, Pi, a_i, a_pi): - """Update distribution from (s[0], z[0], a[-1]) to (s[1], z[1], a[0])""" - # update with continuous policy of last period - D4 = utils.forward_step.forward_step_cpol(D3_prev, a_i, a_pi) - - # update with exogenous shocks today - D2 = utils.forward_step.forward_step_exo(D4, Pi) - - # update with discrete choice today - D3 = utils.forward_step.forward_step_dpol(D2, P) - return D3 - - def forward_step_shock(self, D0, pol_i, pol_pi_shock, Pi, P): - """Forward_step linearized wrt pol_pi.""" - D4 = utils.forward_step.forward_step_cpol_shock(D0, pol_i, pol_pi_shock) - D2 = utils.forward_step.forward_step_exo(D4, Pi) - D3 = utils.forward_step.forward_step_dpol(D2, P) - return D3 - - def forward_step_transpose(self, D, P, Pi, a_i, a_pi): - """Transpose of forward_step.""" - D1 = np.einsum('sza,xsza->xza', D, P) - D2 = np.einsum('xpa,zp->xza', D1, Pi[1]) - D3 = np.einsum('xza,sx->sza', D2, Pi[0]) - D4 = utils.forward_step.forward_step_cpol_transpose(D3, a_i, a_pi) - return D4 - - -def hetoutput(custom_aggregation=None): - def decorator(f): - return HetOutput(f, custom_aggregation=custom_aggregation) - return decorator - - -class HetOutput: - def __init__(self, f, custom_aggregation=None): - self.name = f.__name__ - self.f = f - self.eval_input_list = utils.misc.input_list(f) - - self.custom_aggregation = custom_aggregation - self.agg_input_list = [] if custom_aggregation is None else utils.misc.input_list(custom_aggregation) - - # We are distinguishing between the eval_input_list and agg_input_list because custom aggregation may require - # certain arguments that are not required for simply evaluating the hetoutput - self.input_list = list(set(self.eval_input_list).union(set(self.agg_input_list))) - self.output_list = utils.misc.output_list(f) - - def evaluate(self, arg_dict): - hetoutputs = dict(zip(self.output_list, utils.misc.make_tuple(self.f(*[arg_dict[i] for i - in self.eval_input_list])))) - return hetoutputs - - def aggregate(self, hetoutputs, D, custom_aggregation_args, mode="ss"): - if self.custom_aggregation is not None: - hetoutputs_w_std_aggregation = list(set(self.output_list) - - set([utils.misc.uncapitalize(o) for o - in utils.misc.output_list(self.custom_aggregation)])) - hetoutputs_w_custom_aggregation = list(set(self.output_list) - set(hetoutputs_w_std_aggregation)) - else: - hetoutputs_w_std_aggregation = self.output_list - hetoutputs_w_custom_aggregation = [] - - # TODO: May need to check if this works properly for td - if self.custom_aggregation is not None: - hetoutputs_w_custom_aggregation_args = dict(zip(hetoutputs_w_custom_aggregation, - [hetoutputs[i] for i in hetoutputs_w_custom_aggregation])) - custom_agg_inputs = {"D": D, **hetoutputs_w_custom_aggregation_args, **custom_aggregation_args} - custom_aggregates = dict(zip([o.capitalize() for o in hetoutputs_w_custom_aggregation], - utils.misc.make_tuple(self.custom_aggregation(*[custom_agg_inputs[i] for i - in self.agg_input_list])))) - else: - custom_aggregates = {} - - if mode == "ss": - std_aggregates = {o.capitalize(): np.vdot(D, hetoutputs[o]) for o in hetoutputs_w_std_aggregation} - elif mode == "td": - std_aggregates = {o.capitalize(): utils.optimized_routines.fast_aggregate(D, hetoutputs[o]) - for o in hetoutputs_w_std_aggregation} - else: - raise RuntimeError(f"Mode {mode} is not supported in HetOutput aggregation. Choose either 'ss' or 'td'") - - return {**std_aggregates, **custom_aggregates} diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 86f285b..1facc2f 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -2,15 +2,14 @@ import numpy as np from typing import Optional, Dict -from .support.impulse import ImpulseDict -from .support.bijection import Bijection -from ..primitives import Block +from .block import Block from .. import utilities as utils -from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict -from .support.bijection import Bijection +from ..classes.steady_state_dict import SteadyStateDict +from ..classes.impulse_dict import ImpulseDict +from ..classes.jacobian_dict import JacobianDict from ..utilities.function import ExtendedFunction, ExtendedParallelFunction from ..utilities.ordered_set import OrderedSet +from ..utilities.bijection import Bijection from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition, Transition diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 21dcc3e..882b7af 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -5,10 +5,11 @@ from copy import deepcopy from .support.simple_displacement import ignore, Displace, AccumulatedDerivative -from .support.impulse import ImpulseDict -from ..primitives import Block -from ..steady_state.classes import SteadyStateDict -from ..jacobian.classes import JacobianDict, SimpleSparse, ZeroMatrix +from .block import Block +from ..classes.steady_state_dict import SteadyStateDict +from ..classes.impulse_dict import ImpulseDict +from ..classes.jacobian_dict import JacobianDict +from ..classes.sparse_jacobians import SimpleSparse, ZeroMatrix from ..utilities import misc from ..utilities.function import ExtendedFunction diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 9dafac7..2dc25a9 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -1,10 +1,8 @@ -from sequence_jacobian.utilities.ordered_set import OrderedSet - -from ..primitives import Block -from ..blocks.simple_block import simple -from ..blocks.parent import Parent - -from ..jacobian.classes import FactoredJacobianDict +from .block import Block +from .simple_block import simple +from .support.parent import Parent +from ..classes.jacobian_dict import FactoredJacobianDict +from ..utilities.ordered_set import OrderedSet def solved(unknowns, targets, solver=None, solver_kwargs={}, name=""): diff --git a/src/sequence_jacobian/blocks/parent.py b/src/sequence_jacobian/blocks/support/parent.py similarity index 100% rename from src/sequence_jacobian/blocks/parent.py rename to src/sequence_jacobian/blocks/support/parent.py diff --git a/src/sequence_jacobian/steady_state/support.py b/src/sequence_jacobian/blocks/support/steady_state.py similarity index 99% rename from src/sequence_jacobian/steady_state/support.py rename to src/sequence_jacobian/blocks/support/steady_state.py index e909189..0ad7c6b 100644 --- a/src/sequence_jacobian/steady_state/support.py +++ b/src/sequence_jacobian/blocks/support/steady_state.py @@ -6,8 +6,8 @@ from numbers import Real from functools import partial -from .classes import SteadyStateDict -from ..utilities import misc, solvers +from ...classes.steady_state_dict import SteadyStateDict +from ...utilities import misc, solvers @@ -16,7 +16,7 @@ def augment_dag_w_helper_blocks(dag, calibration, unknowns, targets, helper_bloc to solve the provided set of helper targets analytically, reducing the number of unknowns/targets that need to be solved for numerically. """ - from ..blocks.auxiliary_blocks.calibration_block import CalibrationBlock + from ..auxiliary_blocks.calibration_block import CalibrationBlock targets = {t: 0. for t in targets} if isinstance(targets, list) else targets helper_targets = {t: targets[t] for t in helper_targets} if isinstance(helper_targets, list) else helper_targets diff --git a/src/sequence_jacobian/blocks/support/impulse.py b/src/sequence_jacobian/classes/impulse_dict.py similarity index 97% rename from src/sequence_jacobian/blocks/support/impulse.py rename to src/sequence_jacobian/classes/impulse_dict.py index 430e1b0..774aec2 100644 --- a/src/sequence_jacobian/blocks/support/impulse.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -3,10 +3,9 @@ import numpy as np from copy import deepcopy -from sequence_jacobian.utilities.ordered_set import OrderedSet - -from ...steady_state.classes import SteadyStateDict -from .bijection import Bijection +from ..utilities.ordered_set import OrderedSet +from ..utilities.bijection import Bijection +from .steady_state_dict import SteadyStateDict class ImpulseDict: diff --git a/src/sequence_jacobian/jacobian/classes.py b/src/sequence_jacobian/classes/jacobian_dict.py similarity index 58% rename from src/sequence_jacobian/jacobian/classes.py rename to src/sequence_jacobian/classes/jacobian_dict.py index 77aefcd..548079a 100644 --- a/src/sequence_jacobian/jacobian/classes.py +++ b/src/sequence_jacobian/classes/jacobian_dict.py @@ -1,284 +1,17 @@ -"""Various classes to support the computation of Jacobians""" - -from abc import ABCMeta import copy import warnings import numpy as np -from . import support from ..utilities.misc import factor, factored_solve from ..utilities.ordered_set import OrderedSet -from ..blocks.support.bijection import Bijection -from ..blocks.support.impulse import ImpulseDict +from ..utilities.bijection import Bijection +from .impulse_dict import ImpulseDict +from .sparse_jacobians import ZeroMatrix, IdentityMatrix, SimpleSparse, make_matrix from typing import Any, Dict, Union -# Basic types Array = Any - -class Jacobian(metaclass=ABCMeta): - """An abstract base class encompassing all valid types representing Jacobians, which include - np.ndarray, IdentityMatrix, ZeroMatrix, and SimpleSparse.""" - pass - -# Make np.ndarray a child class of Jacobian -Jacobian.register(np.ndarray) - - -class IdentityMatrix(Jacobian): - """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, - use to initialize Jacobian of a variable wrt itself""" - __array_priority__ = 10_000 - - def sparse(self): - """Equivalent SimpleSparse representation, less efficient operations but more general.""" - return SimpleSparse({(0, 0): 1}) - - def matrix(self, T): - return np.eye(T) - - def __matmul__(self, other): - """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" - return copy.deepcopy(other) - - def __rmatmul__(self, other): - return copy.deepcopy(other) - - def __mul__(self, a): - return a*self.sparse() - - def __rmul__(self, a): - return self.sparse()*a - - def __add__(self, x): - return self.sparse() + x - - def __radd__(self, x): - return x + self.sparse() - - def __sub__(self, x): - return self.sparse() - x - - def __rsub__(self, x): - return x - self.sparse() - - def __neg__(self): - return -self.sparse() - - def __pos__(self): - return self - - def __repr__(self): - return 'IdentityMatrix' - - -class ZeroMatrix(Jacobian): - """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, - use in common case where some outputs don't depend on inputs""" - __array_priority__ = 10_000 - - def sparse(self): - return SimpleSparse({(0, 0): 0}) - - def matrix(self, T): - return np.zeros((T,T)) - - def __matmul__(self, other): - if isinstance(other, np.ndarray) and other.ndim == 1: - return np.zeros_like(other) - else: - return self - - def __rmatmul__(self, other): - return self @ other - - def __mul__(self, a): - return self - - def __rmul__(self, a): - return self - - # copies seem inefficient here, try to live without them - def __add__(self, x): - return x - - def __radd__(self, x): - return x - - def __sub__(self, x): - return -x - - def __rsub__(self, x): - return x - - def __neg__(self): - return self - - def __pos__(self): - return self - - def __repr__(self): - return 'ZeroMatrix' - - -class SimpleSparse(Jacobian): - """Efficient representation of sparse linear operators, which are linear combinations of basis - operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s - (measured by # above main diagonal) and m is number of initial entries missing. - - Examples of such basis operators: - - (0, 0) is identity operator - - (0, 2) is identity operator with first two '1's on main diagonal missing - - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator - - (-1, 1) has 1s on diagonal below main diagonal, except first column - - The linear combination of these basis operators that makes up a given SimpleSparse object is - stored as a dict 'elements' mapping (i, m) -> x. - - The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need - the more general basis (i, m) to ensure closure under multiplication. - - These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space - Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation - for writing SimpleBlock functions. - - The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix - operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less - interchangeably with ordinary NumPy matrices. - """ - - # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules - __array_priority__ = 1000 - - def __init__(self, elements): - self.elements = elements - self.indices, self.xs = None, None - - @staticmethod - def from_simple_diagonals(elements): - """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" - return SimpleSparse({(i, 0): x for i, x in elements.items()}) - - def matrix(self, T): - """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" - return self + np.zeros((T, T)) - - def array(self): - """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) - and one size-N array of floats with entries x. - - This is needed for Numba to take as input. Cache for efficiency. - """ - if self.indices is not None: - return self.indices, self.xs - else: - indices, xs = zip(*self.elements.items()) - self.indices, self.xs = np.array(indices), np.array(xs) - return self.indices, self.xs - - @property - def T(self): - """Transpose""" - return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) - - @property - def iszero(self): - return not self.nonzero().elements - - def nonzero(self): - elements = self.elements.copy() - for im, x in self.elements.items(): - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - return SimpleSparse(elements) - - def __pos__(self): - return self - - def __neg__(self): - return SimpleSparse({im: -x for im, x in self.elements.items()}) - - def __matmul__(self, A): - if isinstance(A, SimpleSparse): - # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs - return SimpleSparse(support.multiply_rs_rs(self, A)) - elif isinstance(A, np.ndarray): - # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing - indices, xs = self.array() - if A.ndim == 2: - return support.multiply_rs_matrix(indices, xs, A) - elif A.ndim == 1: - return support.multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] - else: - return NotImplemented - else: - return NotImplemented - - def __rmatmul__(self, A): - # multiplication rule when this object is on right (will only be called when left is matrix) - # for simplicity, just use transpose to reduce this to previous cases - return (self.T @ A.T).T - - def __add__(self, A): - if isinstance(A, SimpleSparse): - # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap - elements = self.elements.copy() - for im, x in A.elements.items(): - if im in elements: - elements[im] += x - # safeguard to retain sparsity: disregard extremely small elements (num error) - if abs(elements[im]) < 1E-14: - del elements[im] - else: - elements[im] = x - return SimpleSparse(elements) - else: - # add SimpleSparse to T*T matrix - if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: - return NotImplemented - T = A.shape[0] - - # fancy trick to do this efficiently by writing A as flat vector - # then (i, m) can be mapped directly to NumPy slicing! - A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy - for (i, m), x in self.elements.items(): - if i < 0: - A[T * (-i) + (T + 1) * m::T + 1] += x - else: - A[i + (T + 1) * m:(T - i) * T:T + 1] += x - return A.reshape((T, T)) - - def __radd__(self, A): - try: - return self + A - except: - print(self) - print(A) - raise - - def __sub__(self, A): - # slightly inefficient implementation with temporary for simplicity - return self + (-A) - - def __rsub__(self, A): - return -self + A - - def __mul__(self, a): - if not np.isscalar(a): - return NotImplemented - return SimpleSparse({im: a * x for im, x in self.elements.items()}) - - def __rmul__(self, a): - return self * a - - def __repr__(self): - formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' - return f'SimpleSparse({formatted})' - - def __eq__(self, s): - return self.elements == s.elements - +Jacobian = Union[np.ndarray, ZeroMatrix, IdentityMatrix, SimpleSparse] class NestedDict: def __init__(self, nesteddict, outputs: OrderedSet=None, inputs: OrderedSet=None, name: str=None): @@ -380,8 +113,9 @@ def deduplicate(mylist): class JacobianDict(NestedDict): - def __init__(self, nesteddict, outputs=None, inputs=None, name=None, T=None): - ensure_valid_jacobiandict(nesteddict) + def __init__(self, nesteddict, outputs=None, inputs=None, name=None, T=None, check=False): + if check: + ensure_valid_jacobiandict(nesteddict) super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) self.T = T @@ -474,7 +208,7 @@ def pack(self, T=None): J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) for iO, O in enumerate(self.outputs): for iI, I in enumerate(self.inputs): - J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = support.make_matrix(self[O, I], T) + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(self[O, I], T) return J @staticmethod diff --git a/src/sequence_jacobian/classes/sparse_jacobians.py b/src/sequence_jacobian/classes/sparse_jacobians.py new file mode 100644 index 0000000..4cdeb55 --- /dev/null +++ b/src/sequence_jacobian/classes/sparse_jacobians.py @@ -0,0 +1,337 @@ +import numpy as np +from numba import njit +import copy + +class IdentityMatrix: + """Simple identity matrix class, cheaper than using actual np.eye(T) matrix, + use to initialize Jacobian of a variable wrt itself""" + __array_priority__ = 10_000 + + def sparse(self): + """Equivalent SimpleSparse representation, less efficient operations but more general.""" + return SimpleSparse({(0, 0): 1}) + + def matrix(self, T): + return np.eye(T) + + def __matmul__(self, other): + """Identity matrix knows to simply return 'other' whenever it's multiplied by 'other'.""" + return copy.deepcopy(other) + + def __rmatmul__(self, other): + return copy.deepcopy(other) + + def __mul__(self, a): + return a*self.sparse() + + def __rmul__(self, a): + return self.sparse()*a + + def __add__(self, x): + return self.sparse() + x + + def __radd__(self, x): + return x + self.sparse() + + def __sub__(self, x): + return self.sparse() - x + + def __rsub__(self, x): + return x - self.sparse() + + def __neg__(self): + return -self.sparse() + + def __pos__(self): + return self + + def __repr__(self): + return 'IdentityMatrix' + + +class ZeroMatrix: + """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, + use in common case where some outputs don't depend on inputs""" + __array_priority__ = 10_000 + + def sparse(self): + return SimpleSparse({(0, 0): 0}) + + def matrix(self, T): + return np.zeros((T,T)) + + def __matmul__(self, other): + if isinstance(other, np.ndarray) and other.ndim == 1: + return np.zeros_like(other) + else: + return self + + def __rmatmul__(self, other): + return self @ other + + def __mul__(self, a): + return self + + def __rmul__(self, a): + return self + + # copies seem inefficient here, try to live without them + def __add__(self, x): + return x + + def __radd__(self, x): + return x + + def __sub__(self, x): + return -x + + def __rsub__(self, x): + return x + + def __neg__(self): + return self + + def __pos__(self): + return self + + def __repr__(self): + return 'ZeroMatrix' + + +class SimpleSparse: + """Efficient representation of sparse linear operators, which are linear combinations of basis + operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s + (measured by # above main diagonal) and m is number of initial entries missing. + + Examples of such basis operators: + - (0, 0) is identity operator + - (0, 2) is identity operator with first two '1's on main diagonal missing + - (1, 0) has 1s on diagonal above main diagonal: "left-shift" operator + - (-1, 1) has 1s on diagonal below main diagonal, except first column + + The linear combination of these basis operators that makes up a given SimpleSparse object is + stored as a dict 'elements' mapping (i, m) -> x. + + The Jacobian of a SimpleBlock is a SimpleSparse operator combining basis elements (i, 0). We need + the more general basis (i, m) to ensure closure under multiplication. + + These (i, m) correspond to the Q_(-i, m) operators defined for Proposition 2 of the Sequence Space + Jacobian paper. The flipped sign in the code is so that the index 'i' matches the k(i) notation + for writing SimpleBlock functions. + + The "dunder" methods x.__add__(y), x.__matmul__(y), x.__rsub__(y), etc. in Python implement infix + operations x + y, x @ y, y - x, etc. Defining these allows us to use these more-or-less + interchangeably with ordinary NumPy matrices. + """ + + # when performing binary operations on SimpleSparse and a NumPy array, use SimpleSparse's rules + __array_priority__ = 1000 + + def __init__(self, elements): + self.elements = elements + self.indices, self.xs = None, None + + @staticmethod + def from_simple_diagonals(elements): + """Take dict i -> x, i.e. from SimpleBlock differentiation, convert to SimpleSparse (i, 0) -> x""" + return SimpleSparse({(i, 0): x for i, x in elements.items()}) + + def matrix(self, T): + """Return matrix giving first T rows and T columns of matrix representation of SimpleSparse""" + return self + np.zeros((T, T)) + + def array(self): + """Rewrite dict (i, m) -> x as pair of NumPy arrays, one size-N*2 array of ints with rows (i, m) + and one size-N array of floats with entries x. + + This is needed for Numba to take as input. Cache for efficiency. + """ + if self.indices is not None: + return self.indices, self.xs + else: + indices, xs = zip(*self.elements.items()) + self.indices, self.xs = np.array(indices), np.array(xs) + return self.indices, self.xs + + @property + def T(self): + """Transpose""" + return SimpleSparse({(-i, m): x for (i, m), x in self.elements.items()}) + + @property + def iszero(self): + return not self.nonzero().elements + + def nonzero(self): + elements = self.elements.copy() + for im, x in self.elements.items(): + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + return SimpleSparse(elements) + + def __pos__(self): + return self + + def __neg__(self): + return SimpleSparse({im: -x for im, x in self.elements.items()}) + + def __matmul__(self, A): + if isinstance(A, SimpleSparse): + # multiply SimpleSparse by SimpleSparse, simple analytical rules in multiply_rs_rs + return SimpleSparse(multiply_rs_rs(self, A)) + elif isinstance(A, np.ndarray): + # multiply SimpleSparse by matrix or vector, multiply_rs_matrix uses slicing + indices, xs = self.array() + if A.ndim == 2: + return multiply_rs_matrix(indices, xs, A) + elif A.ndim == 1: + return multiply_rs_matrix(indices, xs, A[:, np.newaxis])[:, 0] + else: + return NotImplemented + else: + return NotImplemented + + def __rmatmul__(self, A): + # multiplication rule when this object is on right (will only be called when left is matrix) + # for simplicity, just use transpose to reduce this to previous cases + return (self.T @ A.T).T + + def __add__(self, A): + if isinstance(A, SimpleSparse): + # add SimpleSparse to SimpleSparse, combining dicts, summing x when (i, m) overlap + elements = self.elements.copy() + for im, x in A.elements.items(): + if im in elements: + elements[im] += x + # safeguard to retain sparsity: disregard extremely small elements (num error) + if abs(elements[im]) < 1E-14: + del elements[im] + else: + elements[im] = x + return SimpleSparse(elements) + else: + # add SimpleSparse to T*T matrix + if not isinstance(A, np.ndarray) or A.ndim != 2 or A.shape[0] != A.shape[1]: + return NotImplemented + T = A.shape[0] + + # fancy trick to do this efficiently by writing A as flat vector + # then (i, m) can be mapped directly to NumPy slicing! + A = A.flatten() # use flatten, not ravel, since we'll modify A and want a copy + for (i, m), x in self.elements.items(): + if i < 0: + A[T * (-i) + (T + 1) * m::T + 1] += x + else: + A[i + (T + 1) * m:(T - i) * T:T + 1] += x + return A.reshape((T, T)) + + def __radd__(self, A): + try: + return self + A + except: + print(self) + print(A) + raise + + def __sub__(self, A): + # slightly inefficient implementation with temporary for simplicity + return self + (-A) + + def __rsub__(self, A): + return -self + A + + def __mul__(self, a): + if not np.isscalar(a): + return NotImplemented + return SimpleSparse({im: a * x for im, x in self.elements.items()}) + + def __rmul__(self, a): + return self * a + + def __repr__(self): + formatted = '{' + ', '.join(f'({i}, {m}): {x:.3f}' for (i, m), x in self.elements.items()) + '}' + return f'SimpleSparse({formatted})' + + def __eq__(self, s): + return self.elements == s.elements + + +def multiply_basis(t1, t2): + """Matrix multiplication operation mapping two sparse basis elements to another.""" + # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with + # signs of i and j flipped to reflect different sign convention used here + i, m = t1 + j, n = t2 + k = i + j + if i >= 0: + if j >= 0: + l = max(m, n - i) + elif k >= 0: + l = max(m, n - k) + else: + l = max(m + k, n) + else: + if j <= 0: + l = max(m + j, n) + else: + l = max(m, n) + min(-i, j) + return k, l + + +def multiply_rs_rs(s1, s2): + """Matrix multiplication operation on two SimpleSparse objects.""" + # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, + # add all pairwise products to get overall product + elements = {} + for im, x in s1.elements.items(): + for jn, y in s2.elements.items(): + kl = multiply_basis(im, jn) + if kl in elements: + elements[kl] += x * y + else: + elements[kl] = x * y + return elements + + +@njit +def multiply_rs_matrix(indices, xs, A): + """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. + Much more computationally demanding than multiplying two SimpleSparse (which is almost + free with simple analytical formula), so we implement as jitted function.""" + n = indices.shape[0] + T = A.shape[0] + S = A.shape[1] + Aout = np.zeros((T, S)) + + for count in range(n): + # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' + # was stored in 'indices' and 'xs' + i = indices[count, 0] + m = indices[count, 1] + x = xs[count] + + # loop faster than vectorized when jitted + # directly use def of basis element (i, m), displacement of i and ignore first m + if i == 0: + for t in range(m, T): + for s in range(S): + Aout[t, s] += x * A[t, s] + elif i > 0: + for t in range(m, T - i): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + else: + for t in range(m - i, T): + for s in range(S): + Aout[t, s] += x * A[t + i, s] + return Aout + + +def make_matrix(A, T): + """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method + to convert it to T*T array.""" + if not isinstance(A, np.ndarray): + return A.matrix(T) + else: + return A diff --git a/src/sequence_jacobian/steady_state/classes.py b/src/sequence_jacobian/classes/steady_state_dict.py similarity index 95% rename from src/sequence_jacobian/steady_state/classes.py rename to src/sequence_jacobian/classes/steady_state_dict.py index 8bb5f70..4767e3a 100644 --- a/src/sequence_jacobian/steady_state/classes.py +++ b/src/sequence_jacobian/classes/steady_state_dict.py @@ -1,10 +1,8 @@ -"""Various classes to support the computation of steady states""" - from copy import deepcopy from ..utilities.misc import dict_diff from ..utilities.ordered_set import OrderedSet -from ..blocks.support.bijection import Bijection +from ..utilities.bijection import Bijection import numpy as np diff --git a/src/sequence_jacobian/devtools/__init__.py b/src/sequence_jacobian/devtools/__init__.py deleted file mode 100644 index 3f21969..0000000 --- a/src/sequence_jacobian/devtools/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Tools for debugging, developing, and deprecating code""" - -from . import analysis, debug, deprecate diff --git a/src/sequence_jacobian/devtools/analysis.py b/src/sequence_jacobian/devtools/analysis.py deleted file mode 100644 index 77e90d8..0000000 --- a/src/sequence_jacobian/devtools/analysis.py +++ /dev/null @@ -1,242 +0,0 @@ -"""Low-level tools/classes for analyzing sequence-jacobian model DAGs to support debugging""" - -import numpy as np -import xarray as xr -from collections.abc import Iterable - -from ..utilities import graph - - -class BlockIONetwork: - """ - A 3-d axis-labeled DataArray (blocks x inputs x outputs), which allows for the analysis of the input-output - structure of a DAG. - """ - def __init__(self, blocks, calibration=None, ignore_helpers=True): - topsorted, inset, outset = graph.block_sort(blocks, return_io=True, calibration=calibration, - ignore_helpers=ignore_helpers) - self.blocks = {b.name: b for b in blocks} - self.blocks_names = list(self.blocks.keys()) - self.blocks_as_list = list(self.blocks.values()) - self.var_names = list(inset.union(outset)) - self.darray = xr.DataArray(np.zeros((len(blocks), len(self.var_names), len(self.var_names))), - coords=[self.blocks_names, self.var_names, self.var_names], - dims=["blocks", "inputs", "outputs"]) - - def __repr__(self): - return self.darray.__repr__() - - # User-facing "print" methods - def print_block_links(self, block_name): - print(f" Links in {block_name}") - print(" " + "-" * (len(f" Links in {block_name}"))) - - link_inds = np.nonzero(self._subset_by_block(block_name)).data - links = [] - for i in range(np.shape(link_inds)[0]): - i_ind, o_ind = link_inds[:, i] - i_var = str(self._subset_by_block(block_name).coords["inputs"][i_ind].data) - o_var = str(self._subset_by_block(block_name).coords["outputs"][o_ind].data) - links.append([i_var, o_var]) - self._print_links(links) - - def print_all_var_links(self, var_name, calibration=None, ignore_helpers=True): - print(f" Links from {var_name}") - print(" " + "-" * (len(f" Links for {var_name}"))) - - links = self.find_all_var_links(var_name, calibration=calibration, ignore_helpers=ignore_helpers) - self._print_links(links) - - @staticmethod - def _print_links(links): - link_strs = [] - # Create " -> " linked strings and sort them for nicer printing - for link in links: - link_strs.append(" " + " -> ".join(link)) - link_strs.sort() - for link_str in link_strs: - print(link_str) - print("") # To break lines - - def print_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): - print(f" Links between {unknowns} and {targets}") - print(" " + "-" * (len(f" Links between {unknowns} and {targets}"))) - unknown_target_net = self.find_unknowns_targets_links(unknowns, targets, calibration=calibration, - ignore_helpers=ignore_helpers) - print(unknown_target_net) - print("") # To break lines - - # TODO: Implement an enhancement to display the "closest" link if missing. - def query_var_link(self, input_var, output_var, calibration=None, ignore_helpers=True): - all_links = self.find_all_var_links(input_var, calibration=calibration, ignore_helpers=ignore_helpers) - link_paths = [] - for link in all_links: - if link[0] == input_var and link[-1] == output_var: - link_paths.append(link) - if link_paths: - print(f" Links between {input_var} and {output_var}") - print(" " + "-" * (len(f" Links between {input_var} and {output_var}"))) - self._print_links(link_paths) - else: - print(f"There are no links within the DAG connecting {input_var} to {output_var}") - - # User-facing "analysis" methods - def record_input_variables_paths(self, inputs_to_be_recorded, block_input_args, - calibration=None, ignore_helpers=True): - """ - Updates the VariableIONetwork with the paths that a set of inputs influence, as they propagate through the DAG - - Parameters - ---------- - inputs_to_be_recorded: `list(str)` - A list of input variable names, whose paths will be traced and recorded in the VariableIONetwork - block_input_args: `dict` - A dict of variable/parameter names and values (typically from the steady state of the model) on which, - a block can perform a valid evaluation - calibration: `dict` or None - Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies - ignore_helpers: bool - Refer to `block_sort` docstring on using this to reconcile HelperBlock cyclic dependencies - """ - block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, ignore_helpers=ignore_helpers) - for input_var in inputs_to_be_recorded: - all_input_vars = set(input_var) - for ib in block_inds_sorted: - ib_input_args = {k: v for k, v in block_input_args.items() if k in self.blocks_as_list[ib].inputs} - # This extra step is needed because some arguments required for calling .jac on - # HetBlock and SolvedBlock are not a part of .inputs - ib_input_args.update(**self.blocks_as_list[ib].ss(**ib_input_args)) - io_links = find_io_links(self.blocks_as_list[ib], list(all_input_vars), ib_input_args) - if io_links: - self._record_io_links(self.blocks_names[ib], io_links) - # Need to also track the paths of outputs which could be intermediate inputs further down the DAG - all_input_vars = all_input_vars.union(set(io_links.keys())) - - def find_all_var_links(self, var_name, calibration=None, ignore_helpers=True): - # Find the indices of *direct* links between `var_name` and the affected `outputs`/`blocks` containing those - # `outputs` and instantiate the initial list of links - link_inds = np.nonzero(self._subset_by_vars(var_name).data) - links = [[var_name] for i in range(len(link_inds[0]))] - - block_inds_sorted = graph.block_sort(self.blocks_as_list, calibration=calibration, - ignore_helpers=ignore_helpers) - required = graph.find_outputs_that_are_intermediate_inputs(self.blocks_as_list, ignore_helpers=ignore_helpers) - intermediates = set() - for ib in block_inds_sorted: - # Note: This block is ordered before the bottom block of code since the intermediate outputs from a block - # `ib` do not need to be checked as inputs to that same block `ib`, only the subsequent blocks - if intermediates: - intm_link_inds = np.nonzero(self._subset_by_vars(list(intermediates)).data) - # If there are `intermediate` inputs that have been recorded, we need to find the *indirect* links - for iil, iilb in enumerate(intm_link_inds[0]): - # Check if those inputs are inputs to block `ib` - if ib == iilb: - # If so, repeat the logic from below, where you find the input-output var link - o_var = str(self._subset_by_vars(list(intermediates)).coords["outputs"][intm_link_inds[2][iil]].data) - intm_i_var = str(self._subset_by_vars(list(intermediates)).coords["inputs"][intm_link_inds[1][iil]].data) - - # And add it to the set of all links, recording this new links' output if it hasn't appeared - # before and if it is an intermediate input - links.append([intm_i_var, o_var]) - if o_var in required: - intermediates = intermediates.union([o_var]) - - # Check if `var_name` enters into that block as an input, and if so add the link between it and the output - # it directly affects, recording that output as an intermediate input if needed - # Note: link_inds' 0-th row indicates the blocks and 1st row indicates the outputs - for il, ilb in enumerate(link_inds[0]): - if ib == ilb: - o_var = str(self._subset_by_vars(var_name).coords["outputs"][link_inds[1][il]].data) - links[il].append(o_var) - if o_var in required: - intermediates = intermediates.union([o_var]) - return _compose_dyad_links(links) - - def find_unknowns_targets_links(self, unknowns, targets, calibration=None, ignore_helpers=True): - unknown_target_net = xr.DataArray(np.zeros((len(unknowns), len(targets))), - coords=[unknowns, targets], - dims=["inputs", "outputs"]) - for u in unknowns: - links = self.find_all_var_links(u, calibration=calibration, ignore_helpers=ignore_helpers) - for link in links: - if link[0] == u and link[-1] in targets: - unknown_target_net.loc[u, link[-1]] = 1. - return unknown_target_net - - # Analysis support methods - def _subset_by_block(self, block_name): - return self.darray.loc[block_name, list(self.blocks[block_name].inputs), list(self.blocks[block_name].outputs)] - - def _subset_by_vars(self, vars_names): - if isinstance(vars_names, Iterable): - return self.darray.loc[[b.name for b in self.blocks.values() if np.any(v in b.inputs for v in vars_names)], - vars_names, :] - else: - return self.darray.loc[[b.name for b in self.blocks.values() if vars_names in b.inputs], vars_names, :] - - def _record_io_links(self, block_name, io_links): - for o, i in io_links.items(): - self.darray.loc[block_name, i, o] = 1. - - -def _compose_dyad_links(links): - links_composed = [] - inds_to_ignore = set() - outputs = set() - - for il, link in enumerate(links): - if il in inds_to_ignore: - continue - if links_composed: - if link[0] in outputs: - # Since `link` has as its input one of the outputs recorded from prior links in `links_composed` - # search through the links in `links_composed` to see which links need to be extended with `link` - # and the other links with the same input as `link` - link_extensions = [] - # Potential link extensions will only be located past the stage il that we are at - for il_e in range(il, len(links)): - if links[il_e][0] == link[0]: - link_extensions.append(links[il_e]) - outputs = outputs.union([links[il_e][-1]]) - inds_to_ignore = inds_to_ignore.union([il_e]) - - links_to_add = [] - inds_to_omit = [] - for il_c, link_c in enumerate(links_composed): - if link_c[-1] == link[0]: - inds_to_omit.append(il_c) - links_to_add.extend([link_c + [ext[-1]] for ext in link_extensions]) - - links_composed = [link_c for i, link_c in enumerate(links_composed) if i not in inds_to_omit] + links_to_add - else: - links_composed.append(link) - outputs = outputs.union([link[-1]]) - else: - links_composed.append(link) - outputs = outputs.union([link[-1]]) - return links_composed - - -def find_io_links(block, input_args, block_input_args): - """ - For a given `block`, see which output arguments the input argument `input_args` affects - - Parameters - ---------- - block: `Block` object - One of the various kinds of `Block` objects (`SimpleBlock`, `HetBlock`, etc.) - input_args: `str` or `list(str)` - The input arguments, whose paths through the block to the output variables we want to see - block_input_args: `dict{str: num}` - The rest of the input arguments required to evaluate the block's Jacobian - - Returns - ------- - links: `dict{str: list(str)}` - A dict with *output arguments* as keys, and the input arguments that affect it as values - """ - J = block.jac(ss=block_input_args, T=2, shock_list=input_args) - links = {} - for o in J.outputs: - links[o] = list(J.nesteddict[o].keys()) - return links diff --git a/src/sequence_jacobian/devtools/debug.py b/src/sequence_jacobian/devtools/debug.py deleted file mode 100644 index 48e22ec..0000000 --- a/src/sequence_jacobian/devtools/debug.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Top-level tools to help users debug their sequence-jacobian code""" - -import warnings -import numpy as np - -from . import analysis -from ..utilities import graph - - -def ensure_computability(blocks, calibration=None, unknowns_ss=None, - exogenous=None, unknowns=None, targets=None, ss=None, - verbose=False, fragile=True, ignore_helpers=True): - # Check if `calibration` and `unknowns` jointly have all of the required variables needed to be able - # to calculate a steady state. If ss is provided, assume the user doesn't need to check this - if calibration and unknowns_ss and not ss: - ensure_all_inputs_accounted_for(blocks, calibration, unknowns_ss, verbose=verbose, fragile=fragile) - - # Check if unknowns and exogenous are not outputs of any blocks, and that targets are not inputs to any blocks - if exogenous and unknowns and targets: - ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous + unknowns, targets, - verbose=verbose, fragile=fragile) - - # Check if there are any "broken" links between unknowns and targets, i.e. if there are any unknowns that don't - # affect any targets, or if there are any targets that aren't affected by any unknowns - if unknowns and targets and ss: - ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=verbose, fragile=fragile) - - -# To ensure that no input argument that is required for one of the blocks to evaluate is missing -def ensure_all_inputs_accounted_for(blocks, calibration, unknowns, verbose=False, fragile=True): - variables_accounted_for = set(unknowns.keys()).union(set(calibration.keys())) - all_inputs = set().union(*[b.inputs for b in blocks]) - required = graph.find_outputs_that_are_intermediate_inputs(blocks) - non_computed_inputs = all_inputs.difference(required) - - variables_unaccounted_for = non_computed_inputs.difference(variables_accounted_for) - if variables_unaccounted_for: - if fragile: - raise RuntimeError(f"The following variables were not listed as unknowns or provided as fixed variables/" - f"parameters: {variables_unaccounted_for}") - else: - warnings.warn(f"\nThe following variables were not listed as unknowns or provided as fixed variables/" - f"parameters: {variables_unaccounted_for}") - if verbose: - print("This DAG accounts for all inputs variables.") - - -def ensure_unknowns_exogenous_and_targets_valid_candidates(blocks, exogenous_unknowns, targets, - verbose=False, fragile=True): - cand_xu, cand_targets = find_candidate_unknowns_and_targets(blocks) - invalid_xu = [] - invalid_targ = [] - for xu in exogenous_unknowns: - if xu not in cand_xu: - invalid_xu.append(xu) - for targ in targets: - if targ not in cand_targets: - invalid_targ.append(targ) - if invalid_xu or invalid_targ: - if fragile: - raise RuntimeError(f"The following exogenous/unknowns are invalid candidates: {invalid_xu}\n" - f"The following targets are invalid candidates: {invalid_targ}") - else: - warnings.warn(f"\nThe following exogenous/unknowns are invalid candidates: {invalid_xu}\n" - f"The following targets are invalid candidates: {invalid_targ}") - if verbose: - print("The provided exogenous/unknowns and targets are all valid candidates for this DAG.") - - -def find_candidate_unknowns_and_targets(block_list, verbose=False): - dep, inputs, outputs = graph.block_sort(block_list, return_io=True) - required = graph.find_outputs_that_are_intermediate_inputs(block_list) - - # Candidate exogenous and unknowns (also includes parameters): inputs that are not outputs of any block - # Candidate targets: outputs that are not inputs to any block - cand_xu = inputs.difference(required) - cand_targets = outputs.difference(required) - - if verbose: - print(f"Candidate exogenous/unknowns: {cand_xu}\n" - f"Candidate targets: {cand_targets}") - - return cand_xu, cand_targets - - -def ensure_unknowns_and_targets_are_valid(blocks, unknowns, targets, ss, verbose=False, fragile=True): - io_net = analysis.BlockIONetwork(blocks) - io_net.record_input_variables_paths(unknowns, ss) - ut_net = io_net.find_unknowns_targets_links(unknowns, targets) - broken_unknowns = [] - broken_targets = [] - for u in unknowns: - if not np.any(ut_net.loc[u, :]): - broken_unknowns.append(u) - for t in targets: - if not np.any(ut_net.loc[:, t]): - broken_targets.append(t) - if broken_unknowns or broken_targets: - if fragile: - raise RuntimeError(f"The following unknowns don't affect any targets: {broken_unknowns}\n" - f"The following targets aren't affected by any unknowns: {broken_targets}") - else: - warnings.warn(f"\nThe following unknowns don't affect any targets: {broken_unknowns}\n" - f"The following targets aren't affected by any unknowns: {broken_targets}") - if verbose: - print("This DAG does not contain any broken links between unknowns and targets.") diff --git a/src/sequence_jacobian/devtools/deprecate.py b/src/sequence_jacobian/devtools/deprecate.py deleted file mode 100644 index e7b4cb1..0000000 --- a/src/sequence_jacobian/devtools/deprecate.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Tools for deprecating older SSJ code conventions in favor of newer conventions""" - -import warnings - -# The code in this module is meant to assist with users who have used past versions of sequence-jacobian, by temporarily -# providing support for old conventions via deprecated methods, providing time to allow for a seamless upgrade -# to newer versions sequence-jacobian. - -# TODO: There are also the .ss, .td, and .jac methods that are deprecated within the various Block class definitions -# themselves. - - -def rename_output_list_to_outputs(outputs=None, output_list=None): - if output_list is not None: - warnings.warn("The output_list kwarg has been deprecated and replaced with the outputs kwarg.", - DeprecationWarning) - if outputs is None: - return output_list - else: - return list(set(outputs) | set(output_list)) - else: - return outputs diff --git a/src/sequence_jacobian/jacobian/__init__.py b/src/sequence_jacobian/jacobian/__init__.py deleted file mode 100644 index 57070a8..0000000 --- a/src/sequence_jacobian/jacobian/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Jacobian computation and support functions""" diff --git a/src/sequence_jacobian/jacobian/support.py b/src/sequence_jacobian/jacobian/support.py deleted file mode 100644 index 8ab3b76..0000000 --- a/src/sequence_jacobian/jacobian/support.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Various lower-level functions to support the computation of Jacobians""" - -import numpy as np -from numba import njit - - -# For supporting SimpleSparse -def multiply_basis(t1, t2): - """Matrix multiplication operation mapping two sparse basis elements to another.""" - # equivalent to formula in Proposition 2 of Sequence Space Jacobian paper, but with - # signs of i and j flipped to reflect different sign convention used here - i, m = t1 - j, n = t2 - k = i + j - if i >= 0: - if j >= 0: - l = max(m, n - i) - elif k >= 0: - l = max(m, n - k) - else: - l = max(m + k, n) - else: - if j <= 0: - l = max(m + j, n) - else: - l = max(m, n) + min(-i, j) - return k, l - - -def multiply_rs_rs(s1, s2): - """Matrix multiplication operation on two SimpleSparse objects.""" - # iterate over all pairs (i, m) -> x and (j, n) -> y in objects, - # add all pairwise products to get overall product - elements = {} - for im, x in s1.elements.items(): - for jn, y in s2.elements.items(): - kl = multiply_basis(im, jn) - if kl in elements: - elements[kl] += x * y - else: - elements[kl] = x * y - return elements - - -@njit -def multiply_rs_matrix(indices, xs, A): - """Matrix multiplication of SimpleSparse object ('indices' and 'xs') and matrix A. - Much more computationally demanding than multiplying two SimpleSparse (which is almost - free with simple analytical formula), so we implement as jitted function.""" - n = indices.shape[0] - T = A.shape[0] - S = A.shape[1] - Aout = np.zeros((T, S)) - - for count in range(n): - # for Numba to jit easily, SimpleSparse with basis elements '(i, m)' with coefs 'x' - # was stored in 'indices' and 'xs' - i = indices[count, 0] - m = indices[count, 1] - x = xs[count] - - # loop faster than vectorized when jitted - # directly use def of basis element (i, m), displacement of i and ignore first m - if i == 0: - for t in range(m, T): - for s in range(S): - Aout[t, s] += x * A[t, s] - elif i > 0: - for t in range(m, T - i): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - else: - for t in range(m - i, T): - for s in range(S): - Aout[t, s] += x * A[t + i, s] - return Aout - - -def pack_vectors(vs, names, T): - v = np.zeros(len(names)*T) - for i, name in enumerate(names): - if name in vs: - v[i*T:(i+1)*T] = vs[name] - return v - - -def unpack_vectors(v, names, T): - vs = {} - for i, name in enumerate(names): - vs[name] = v[i*T:(i+1)*T] - return vs - - -def make_matrix(A, T): - """If A is not an outright ndarray, e.g. it is SimpleSparse, call its .matrix(T) method - to convert it to T*T array.""" - if not isinstance(A, np.ndarray): - return A.matrix(T) - else: - return A diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py index 24970bb..c5ce817 100644 --- a/src/sequence_jacobian/models/hank.py +++ b/src/sequence_jacobian/models/hank.py @@ -203,7 +203,7 @@ def res(x): calibration['beta'], calibration['vphi'] = beta_loc, vphi_loc out = household.steady_state(calibration) - return np.array([out['A'] - B, out['N_e'] - 1]) + return np.array([out['A'] - B, out['N_E'] - 1]) # solve for beta, vphi (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) @@ -218,7 +218,7 @@ def res(x): # add aggregate variables ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, - 'rho_s': rho_s, 'labor_mkt': ss["N_e"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, + 'rho_s': rho_s, 'labor_mkt': ss["N_E"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) return ss diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py index 5eeeb8b..e6efe0a 100644 --- a/src/sequence_jacobian/models/krusell_smith.py +++ b/src/sequence_jacobian/models/krusell_smith.py @@ -4,7 +4,7 @@ from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.het_block import het -from ..steady_state.classes import SteadyStateDict +from ..classes.steady_state_dict import SteadyStateDict '''Part 1: HA block''' diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 0163531..3dc9714 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -392,7 +392,7 @@ def res(x): ss.internal["household"].update({"chi": chi}) ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, - 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'Chi': Chi, 'phi': phi, 'wealth': tot_wealth, + 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'CHI': Chi, 'phi': phi, 'wealth': tot_wealth, 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, diff --git a/src/sequence_jacobian/steady_state/__init__.py b/src/sequence_jacobian/steady_state/__init__.py deleted file mode 100644 index 017e34b..0000000 --- a/src/sequence_jacobian/steady_state/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Steady state computation and support functions""" diff --git a/src/sequence_jacobian/blocks/support/bijection.py b/src/sequence_jacobian/utilities/bijection.py similarity index 97% rename from src/sequence_jacobian/blocks/support/bijection.py rename to src/sequence_jacobian/utilities/bijection.py index 60d8803..31f0098 100644 --- a/src/sequence_jacobian/blocks/support/bijection.py +++ b/src/sequence_jacobian/utilities/bijection.py @@ -1,4 +1,4 @@ -from ...utilities.ordered_set import OrderedSet +from .ordered_set import OrderedSet class Bijection: def __init__(self, map): diff --git a/src/sequence_jacobian/visualization/__init__.py b/src/sequence_jacobian/visualization/__init__.py deleted file mode 100644 index 2850057..0000000 --- a/src/sequence_jacobian/visualization/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Various tools for plotting and creating visualizations""" diff --git a/src/sequence_jacobian/visualization/draw_dag.py b/src/sequence_jacobian/visualization/draw_dag.py deleted file mode 100644 index 9b75ca5..0000000 --- a/src/sequence_jacobian/visualization/draw_dag.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Provides the functionality for basic DAG visualization""" - -import warnings -from ..utilities.graph import block_sort, construct_output_map, construct_dependency_graph - - -# Implement DAG drawing functions as "soft" dependencies to not enforce the installation of graphviz, since -# it's not required for the rest of the sequence-jacobian code to run -try: - """ - DAG Graph routine - Requires installing graphviz package and executables - https://www.graphviz.org/ - - On a mac this can be done as follows: - 1) Download macports at: - https://www.macports.org/install.php - 2) On the command line, install graphviz with macports by typing - sudo port install graphviz - - """ - from graphviz import Digraph - - - def draw_dag(block_list, exogenous=None, unknowns=None, targets=None, ignore_helpers=True, calibration=None, - showdag=False, leftright=False, filename='modeldag'): - """ - Visualizes a Directed Acyclic Graph (DAG) of a set of blocks, exogenous variables, unknowns, and targets - - block_list: `list` - Blocks to be represented as nodes within a DAG - exogenous: `list` (optional) - Exogenous variables, to be represented on DAG - unknowns: `list` (optional) - Unknown variables, to be represented on DAG - targets: `list` (optional) - Target variables, to be represented on DAG - ignore_helpers: `bool` - A boolean indicating whether to also draw HelperBlocks contained in block_list into the DAG - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using HelperBlocks. Read `block_sort` docstring for more detail - showdag: `bool` - If True, export and plot pdf file. If false, export png file and do not plot - leftright: `bool` - If True, plots DAG from left to right instead of top to bottom - - return: None - """ - - # To prevent having mutable variables as keyword arguments - exogenous = [] if exogenous is None else exogenous - unknowns = [] if unknowns is None else unknowns - targets = [] if targets is None else targets - - # obtain the topological sort - topsorted = block_sort(block_list, ignore_helpers=ignore_helpers, calibration=calibration) - # get sorted list of blocks - block_list_sorted = [block_list[i] for i in topsorted] - # Obtain the dependency list of the sorted set of blocks - dep_list_sorted = construct_dependency_graph(block_list_sorted, construct_output_map(block_list_sorted), - ignore_helpers=ignore_helpers, calibration=calibration) - - # Draw DAG - dot = Digraph(comment='Model DAG') - - # Make left-to-right - if leftright: - dot.attr(rankdir='LR', ratio='compress', center='true') - else: - dot.attr(ratio='auto', center='true') - - # add initial nodes (one for exogenous, one for unknowns) provided those are not empty lists - if exogenous: - dot.node('exog', 'exogenous', shape='box') - if unknowns: - dot.node('unknowns', 'unknowns', shape='box') - if targets: - dot.node('targets', 'targets', shape='diamond') - - # add nodes sequentially in order - for i in dep_list_sorted: - if hasattr(block_list_sorted[i], 'hetinput'): - # HA block - dot.node(str(i), 'HA [' + str(i) + ']') - elif hasattr(block_list_sorted[i], 'block_list'): - # Solved block - dot.node(str(i), block_list_sorted[i].block_list[0].f.__name__ + '[solved,' + str(i) + ']') - else: - # Simple block - dot.node(str(i), block_list_sorted[i].f.__name__ + ' [' + str(i) + ']') - - # nodes from exogenous to i (figure out if needed and draw) - if exogenous: - edgelabel = block_list_sorted[i].inputs & set(exogenous) - if len(edgelabel) != 0: - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - dot.edge('exog', str(i), label=str(edgelabel_str)) - - # nodes from unknowns to i (figure out if needed and draw) - if unknowns: - edgelabel = block_list_sorted[i].inputs & set(unknowns) - if len(edgelabel) != 0: - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - dot.edge('unknowns', str(i), label=str(edgelabel_str)) - - # nodes from i to final targets - for target in targets: - if target in block_list_sorted[i].outputs: - dot.edge(str(i), 'targets', label=target) - - # nodes from any interior block to i - for j in dep_list_sorted[i]: - # figure out inputs of i that are also outputs of j - edgelabel = block_list_sorted[i].inputs & block_list_sorted[j].outputs - edgelabel_list = list(edgelabel) - edgelabel_str = ', '.join(str(e) for e in edgelabel_list) - - # draw edge from j to i - dot.edge(str(j), str(i), label=str(edgelabel_str)) - - if showdag: - dot.render('dagexport/' + filename, view=True, cleanup=True) - else: - dot.render('dagexport/' + filename, format='png', cleanup=True) - # print(dot.source) - - - def draw_solved(solvedblock, filename='solveddag'): - # Inspects a solved block by drawing its DAG - draw_dag([solvedblock.block_list[0]], unknowns=solvedblock.unknowns, targets=solvedblock.targets, - filename=filename, showdag=True) - - - def inspect_solved(block_list): - # Inspects all the solved blocks by running through each and drawing its DAG in turn - for block in block_list: - if hasattr(block, 'block_list'): - draw_solved(block, filename=str(block.block_list[0].f.__name__)) -except ImportError: - def draw_dag(*args, **kwargs): - warnings.warn("\nAttempted to use `draw_dag` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass - - - def draw_solved(*args, **kwargs): - warnings.warn("\nAttempted to use `draw_solved` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass - - - def inspect_solved(*args, **kwargs): - warnings.warn("\nAttempted to use `inspect_solved` when the package `graphviz` has not yet been installed. \n" - "DAG visualization tools, i.e. draw_dag, will not produce any figures unless this dependency has been installed. \n" - "Once installed, re-load sequence-jacobian to produce DAG figures.") - pass diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 5c91f21..0d42c3d 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -2,7 +2,7 @@ import numpy as np -from sequence_jacobian.jacobian.classes import JacobianDict +from sequence_jacobian.classes.jacobian_dict import JacobianDict def test_ks_jac(krusell_smith_dag): diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 3f38b78..aef2a77 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -5,7 +5,7 @@ from sequence_jacobian import combine from sequence_jacobian.models import rbc from sequence_jacobian.blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock -from sequence_jacobian.steady_state.classes import SteadyStateDict +from sequence_jacobian import SteadyStateDict def test_jacobian_dict_block_impulses(rbc_dag): diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index 1aea6ce..674523a 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -1,6 +1,5 @@ import numpy as np import sequence_jacobian as sj -from sequence_jacobian.utilities.multidim import multiply_ith_dimension from sequence_jacobian import het, simple, combine diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index d1ea968..0b3c5d9 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -4,10 +4,9 @@ import pytest from sequence_jacobian import het -from sequence_jacobian.steady_state.classes import SteadyStateDict -from sequence_jacobian.blocks.support.impulse import ImpulseDict -from sequence_jacobian.blocks.support.bijection import Bijection - +from sequence_jacobian.classes.steady_state_dict import SteadyStateDict +from sequence_jacobian.classes.impulse_dict import ImpulseDict +from sequence_jacobian.utilities.bijection import Bijection def test_impulsedict(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index ac18a50..16ab5a8 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -5,7 +5,7 @@ import pytest from sequence_jacobian import simple -from sequence_jacobian.steady_state.classes import SteadyStateDict +from sequence_jacobian.classes.steady_state_dict import SteadyStateDict @simple diff --git a/tests/base/test_solved_block.py b/tests/base/test_solved_block.py index a10bd09..6b1e253 100644 --- a/tests/base/test_solved_block.py +++ b/tests/base/test_solved_block.py @@ -1,7 +1,7 @@ import numpy as np from sequence_jacobian import simple, solved -from sequence_jacobian.steady_state.classes import SteadyStateDict -from sequence_jacobian.jacobian.classes import FactoredJacobianDict +from sequence_jacobian.classes.steady_state_dict import SteadyStateDict +from sequence_jacobian.classes.jacobian_dict import FactoredJacobianDict @simple diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 719820e..143b2c9 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -1,7 +1,7 @@ import numpy as np import sequence_jacobian as sj from sequence_jacobian import het, simple, solved, combine, create_model -from sequence_jacobian.blocks.support.impulse import ImpulseDict +from sequence_jacobian.classes.impulse_dict import ImpulseDict '''Part 1: Household block''' @@ -123,13 +123,13 @@ def test_all(): dag0 = create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') ss0 = dag0.solve_steady_state(calibration, solver='hybr', unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) + targets={'asset_mkt': 0.0, 'tau': 0.334, 'MPC': 0.25}) # DAG with SolvedBlock `fiscal_solved` dag1 = create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') ss1 = dag1.solve_steady_state(calibration, solver='hybr', dissolve=['fiscal_solved'], unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'Mpc': 0.25}) + targets={'asset_mkt': 0.0, 'tau': 0.334, 'MPC': 0.25}) assert all(np.allclose(ss0[k], ss1[k]) for k in ss0) @@ -137,16 +137,18 @@ def test_all(): Js = {'household': household.jacobian(ss1, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} # Linear impulse responses from Jacobian vs directly - G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], + G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) td_lin1 = G @ shock td_lin2 = dag1.solve_impulse_linear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], Js=Js) + inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) # Nonlinear vs linear impulses td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'Mpc', 'asset_mkt', 'goods_mkt'], Js=Js) + inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'Mpc') + assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') + +test_all() From 0bf0d178c1aa3206551b214947c089196c83f182 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 14:09:52 -0500 Subject: [PATCH 263/288] class package includes child classes, easier to access them --- .../auxiliary_blocks/jacobiandict_block.py | 3 +- src/sequence_jacobian/blocks/block.py | 4 +- .../blocks/combined_block.py | 4 +- src/sequence_jacobian/blocks/het_block.py | 4 +- src/sequence_jacobian/blocks/simple_block.py | 5 +- src/sequence_jacobian/blocks/solved_block.py | 2 +- src/sequence_jacobian/classes/__init__.py | 4 + .../utilities/forward_step.py | 243 ------------------ 8 files changed, 11 insertions(+), 258 deletions(-) create mode 100644 src/sequence_jacobian/classes/__init__.py delete mode 100644 src/sequence_jacobian/utilities/forward_step.py diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index b361c94..a0e8f58 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -1,8 +1,7 @@ """A simple wrapper for JacobianDicts to be embedded in DAGs""" from ..block import Block -from ...classes.impulse_dict import ImpulseDict -from ...classes.jacobian_dict import JacobianDict +from ...classes import ImpulseDict, JacobianDict class JacobianDictBlock(JacobianDict, Block): """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 9e42bdb..27661eb 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -5,14 +5,12 @@ from typing import Any, Dict, Union, Tuple, Optional, List from copy import deepcopy -from ..classes.steady_state_dict import SteadyStateDict, UserProvidedSS -from ..classes.impulse_dict import ImpulseDict -from ..classes.jacobian_dict import JacobianDict, FactoredJacobianDict from .support.steady_state import provide_solver_default, solve_for_unknowns, compute_target_values from .support.parent import Parent from ..utilities import misc from ..utilities.bijection import Bijection from ..utilities.ordered_set import OrderedSet +from ..classes import SteadyStateDict, UserProvidedSS, ImpulseDict, JacobianDict, FactoredJacobianDict Array = Any diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index cc193fa..063a120 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -3,10 +3,10 @@ from copy import deepcopy from .block import Block -from ..utilities.graph import block_sort, find_intermediate_inputs from .auxiliary_blocks.jacobiandict_block import JacobianDictBlock from .support.parent import Parent -from ..classes.jacobian_dict import JacobianDict +from ..classes import JacobianDict +from ..utilities.graph import block_sort, find_intermediate_inputs def combine(blocks, name="", model_alias=False): diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 1facc2f..1ced971 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -4,9 +4,7 @@ from .block import Block from .. import utilities as utils -from ..classes.steady_state_dict import SteadyStateDict -from ..classes.impulse_dict import ImpulseDict -from ..classes.jacobian_dict import JacobianDict +from ..classes import SteadyStateDict, ImpulseDict, JacobianDict from ..utilities.function import ExtendedFunction, ExtendedParallelFunction from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 882b7af..50bdcd8 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -6,10 +6,7 @@ from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .block import Block -from ..classes.steady_state_dict import SteadyStateDict -from ..classes.impulse_dict import ImpulseDict -from ..classes.jacobian_dict import JacobianDict -from ..classes.sparse_jacobians import SimpleSparse, ZeroMatrix +from ..classes.steady_state_dict import SteadyStateDict, ImpulseDict, JacobianDict, SimpleSparse, ZeroMatrix from ..utilities import misc from ..utilities.function import ExtendedFunction diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 2dc25a9..7c6bfe7 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -1,7 +1,7 @@ from .block import Block from .simple_block import simple from .support.parent import Parent -from ..classes.jacobian_dict import FactoredJacobianDict +from ..classes import FactoredJacobianDict from ..utilities.ordered_set import OrderedSet diff --git a/src/sequence_jacobian/classes/__init__.py b/src/sequence_jacobian/classes/__init__.py new file mode 100644 index 0000000..651bfa0 --- /dev/null +++ b/src/sequence_jacobian/classes/__init__.py @@ -0,0 +1,4 @@ +from .steady_state_dict import SteadyStateDict, UserProvidedSS +from .impulse_dict import ImpulseDict +from .jacobian_dict import JacobianDict, FactoredJacobianDict +from .sparse_jacobians import ZeroMatrix, IdentityMatrix, SimpleSparse diff --git a/src/sequence_jacobian/utilities/forward_step.py b/src/sequence_jacobian/utilities/forward_step.py deleted file mode 100644 index 50d96db..0000000 --- a/src/sequence_jacobian/utilities/forward_step.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Forward iteration of distribution on grid and related functions. - - - forward_step_1d - - forward_step_2d - - apply law of motion for distribution to go from D_{t-1} to D_t - - - forward_step_shock_1d - - forward_step_shock_2d - - forward_step linearized, used in part 1 of fake news algorithm to get curlyDs - - - forward_step_transpose_1d - - forward_step_transpose_2d - - transpose of forward_step, used in part 2 of fake news algorithm to get curlyPs -""" - -import numpy as np -from numba import njit - - -@njit -def forward_step_1d(D, Pi_T, x_i, x_pi): - """Single forward step to update distribution using exogenous Markov transition Pi and - policy x_i and x_pi for one-dimensional endogenous state. - - Efficient implementation of D_t = Lam_{t-1}' @ D_{t-1} using sparsity of the endogenous - part of Lam_{t-1}'. - - Note that it takes Pi_T, the transpose of Pi, as input rather than transposing itself; - this is so that when it is applied repeatedly, we can precalculate a transpose stored in - correct order rather than a view. - - Parameters - ---------- - D : array (S*X), beginning-of-period distribution over s_t, x_(t-1) - Pi_T : array (S*S), transpose Markov matrix that maps s_t to s_(t+1) - x_i : int array (S*X), left gridpoint of endogenous policy - x_pi : array (S*X), weight on left gridpoint of endogenous policy - - Returns - ---------- - Dnew : array (S*X), beginning-of-next-period dist s_(t+1), x_t - """ - - # first update using endogenous policy - nZ, nX = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - i = x_i[iz, ix] - pi = x_pi[iz, ix] - d = D[iz, ix] - Dnew[iz, i] += d * pi - Dnew[iz, i+1] += d * (1 - pi) - - # then using exogenous transition matrix - return Pi_T @ Dnew - - -def forward_step_2d(D, Pi_T, x_i, y_i, x_pi, y_pi): - """Like forward_step_1d but with two-dimensional endogenous state, policies given by x and y""" - Dmid = forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi) - nZ, nX, nY = Dmid.shape - return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) - - -@njit -def forward_step_endo_2d(D, x_i, y_i, x_pi, y_pi): - """Endogenous update part of forward_step_2d""" - nZ, nX, nY = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i[iz, ix, iy] - iyp = y_i[iz, ix, iy] - beta = x_pi[iz, ix, iy] - alpha = y_pi[iz, ix, iy] - - Dnew[iz, ixp, iyp] += alpha * beta * D[iz, ix, iy] - Dnew[iz, ixp+1, iyp] += alpha * (1 - beta) * D[iz, ix, iy] - Dnew[iz, ixp, iyp+1] += (1 - alpha) * beta * D[iz, ix, iy] - Dnew[iz, ixp+1, iyp+1] += (1 - alpha) * (1 - beta) * D[iz, ix, iy] - return Dnew - - -@njit -def forward_step_shock_1d(Dss, Pi_T, x_i_ss, x_pi_shock): - """forward_step_1d linearized wrt x_pi""" - # first find effect of shock to endogenous policy - nZ, nX = Dss.shape - Dshock = np.zeros_like(Dss) - for iz in range(nZ): - for ix in range(nX): - i = x_i_ss[iz, ix] - dshock = x_pi_shock[iz, ix] * Dss[iz, ix] - Dshock[iz, i] += dshock - Dshock[iz, i + 1] -= dshock - - # then apply exogenous transition matrix to update - return Pi_T @ Dshock - - -def forward_step_shock_2d(Dss, Pi_T, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): - """forward_step_2d linearized wrt x_pi and y_pi""" - Dmid = forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock) - nZ, nX, nY = Dmid.shape - return (Pi_T @ Dmid.reshape(nZ, -1)).reshape(nZ, nX, nY) - - -@njit -def forward_step_shock_endo_2d(Dss, x_i_ss, y_i_ss, x_pi_ss, y_pi_ss, x_pi_shock, y_pi_shock): - """Endogenous update part of forward_step_shock_2d""" - nZ, nX, nY = Dss.shape - Dshock = np.zeros_like(Dss) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i_ss[iz, ix, iy] - iyp = y_i_ss[iz, ix, iy] - alpha = x_pi_ss[iz, ix, iy] - beta = y_pi_ss[iz, ix, iy] - - dalpha = x_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] - dbeta = y_pi_shock[iz, ix, iy] * Dss[iz, ix, iy] - - Dshock[iz, ixp, iyp] += dalpha * beta + alpha * dbeta - Dshock[iz, ixp+1, iyp] += dbeta * (1-alpha) - beta * dalpha - Dshock[iz, ixp, iyp+1] += dalpha * (1-beta) - alpha * dbeta - Dshock[iz, ixp+1, iyp+1] -= dalpha * (1-beta) + dbeta * (1-alpha) - return Dshock - - -@njit -def forward_step_transpose_1d(D, Pi, x_i, x_pi): - """Transpose of forward_step_1d""" - # first update using exogenous transition matrix - D = Pi @ D - - # then update using (transpose) endogenous policy - nZ, nX = D.shape - Dnew = np.zeros_like(D) - for iz in range(nZ): - for ix in range(nX): - i = x_i[iz, ix] - pi = x_pi[iz, ix] - Dnew[iz, ix] = pi * D[iz, i] + (1-pi) * D[iz, i+1] - return Dnew - - -def forward_step_transpose_2d(D, Pi, x_i, y_i, x_pi, y_pi): - """Transpose of forward_step_2d.""" - nZ, nX, nY = D.shape - Dmid = (Pi @ D.reshape(nZ, -1)).reshape(nZ, nX, nY) - return forward_step_transpose_endo_2d(Dmid, x_i, y_i, x_pi, y_pi) - - -@njit -def forward_step_transpose_endo_2d(D, x_i, y_i, x_pi, y_pi): - """Endogenous update part of forward_step_transpose_2d""" - nZ, nX, nY = D.shape - Dnew = np.empty_like(D) - for iz in range(nZ): - for ix in range(nX): - for iy in range(nY): - ixp = x_i[iz, ix, iy] - iyp = y_i[iz, ix, iy] - alpha = x_pi[iz, ix, iy] - beta = y_pi[iz, ix, iy] - - Dnew[iz, ix, iy] = (alpha * beta * D[iz, ixp, iyp] + alpha * (1-beta) * D[iz, ixp, iyp+1] + - (1-alpha) * beta * D[iz, ixp+1, iyp] + - (1-alpha) * (1-beta) * D[iz, ixp+1, iyp+1]) - return Dnew - - -''' -For HetDC block. - -D0 : s[-1], z[-1], a[-1] (end of last period) -D1 : x[0], z[-1], a[-1] (employment shock) -D2 : x[0], z[0], a[-1] (productivity shock) -D3 : s[0], z[0], a[-1] (discrete choice) REFERENCE STAGE -D4 : s[0], z[0], a[0] (cont choice) - -''' - - -def forward_step_exo(D0, Pi): - """Update distribution s[-1], z[-1], a[-1] to x[0], z[0], a[-1].""" - D1 = np.einsum('sza,sx->xza', D0, Pi[0]) # s[-1] -> x[0] - D2 = np.einsum('xza,zp->xpa', D1, Pi[1]) # z[-1] -> z[0] - return D2 - - -def forward_step_dpol(D2, P): - """Update distribution x[0], z[0], a[-1] to s[0], z[0], a[-1].""" - D3 = np.einsum('xza,xsza->sza', D2, P) - return D3 - - -@njit -def forward_step_cpol(D3, a_i, a_pi): - """Update distribution s[0], z[0], a[-1] to s[0], z[0], a[0].""" - nS, nZ, nA = D3.shape - D4 = np.zeros_like(D3) - for iw in range(nS): - for iz in range(nZ): - for ia in range(nA): - i = a_i[iw, iz, ia] - pi = a_pi[iw, iz, ia] - d = D3[iw, iz, ia] - D4[iw, iz, i] += d * pi - D4[iw, iz, i+1] += d * (1 - pi) - return D4 - - -@njit -def forward_step_cpol_shock(D3, a_i_ss, a_pi_shock): - """forward_step_cpol linearized wrt a_pi""" - nS, nZ, nA = D3.shape - dD4 = np.zeros_like(D3) - for iw in range(nS): - for iz in range(nZ): - for ia in range(nA): - i = a_i_ss[iw, iz, ia] - dshock = a_pi_shock[iw, iz, ia] * D3[iw, iz, ia] - dD4[iw, iz, i] += dshock - dD4[iw, iz, i + 1] -= dshock - return dD4 - - -@njit -def forward_step_cpol_transpose(D3, a_i, a_pi): - """Transpose of forward_step_cpol""" - nS, nZ, nA = D3.shape - D4 = np.zeros_like(D3) - for iw in range(nS): - for iz in range(nZ): - for ia in range(nA): - i = a_i[iw, iz, ia] - pi = a_pi[iw, iz, ia] - D4[iw, iz, ia] = pi * D3[iw, iz, i] + (1 - pi) * D3[iw, iz, i + 1] - return D4 From dbb1dfd556dce53a528089feb482ae1bdf9f208c Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 14:19:24 -0500 Subject: [PATCH 264/288] fix errors --- src/sequence_jacobian/blocks/simple_block.py | 3 +-- src/sequence_jacobian/utilities/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 50bdcd8..bed741a 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -1,12 +1,11 @@ """Class definition of a simple block""" -import warnings import numpy as np from copy import deepcopy from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .block import Block -from ..classes.steady_state_dict import SteadyStateDict, ImpulseDict, JacobianDict, SimpleSparse, ZeroMatrix +from ..classes import SteadyStateDict, ImpulseDict, JacobianDict, SimpleSparse, ZeroMatrix from ..utilities import misc from ..utilities.function import ExtendedFunction diff --git a/src/sequence_jacobian/utilities/__init__.py b/src/sequence_jacobian/utilities/__init__.py index e8d1bab..04d85a8 100644 --- a/src/sequence_jacobian/utilities/__init__.py +++ b/src/sequence_jacobian/utilities/__init__.py @@ -1,3 +1,4 @@ """Utilities relating to: interpolation, forward step/transition, grids and Markov chains, solvers, sorting, etc.""" -from . import differentiate, discretize, forward_step, graph, interpolate, misc, optimized_routines, solvers +from . import (bijection, differentiate, discretize, function, graph, interpolate, + misc, multidim, optimized_routines, ordered_set, solvers) From 5d5722ff7bcd9ca04089c2aa2fd6879b1737a655 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 9 Sep 2021 17:02:11 -0500 Subject: [PATCH 265/288] added options, but equivalence of impulse_linear and jacobian in test_workflow now broken, still trying to fix --- src/sequence_jacobian/blocks/block.py | 127 +++++++++++------- .../blocks/combined_block.py | 28 ++-- src/sequence_jacobian/blocks/het_block.py | 1 - src/sequence_jacobian/blocks/solved_block.py | 43 +++--- src/sequence_jacobian/classes/impulse_dict.py | 7 +- src/sequence_jacobian/utilities/function.py | 12 +- tests/base/test_jacobian.py | 3 - tests/base/test_options.py | 22 +++ tests/base/test_solved_block.py | 19 +-- 9 files changed, 159 insertions(+), 103 deletions(-) create mode 100644 tests/base/test_options.py diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 27661eb..dc25223 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -8,6 +8,7 @@ from .support.steady_state import provide_solver_default, solve_for_unknowns, compute_target_values from .support.parent import Parent from ..utilities import misc +from ..utilities.function import input_list, input_kwarg_list from ..utilities.bijection import Bijection from ..utilities.ordered_set import OrderedSet from ..classes import SteadyStateDict, UserProvidedSS, ImpulseDict, JacobianDict, FactoredJacobianDict @@ -19,7 +20,18 @@ class Block: def __init__(self): self.M = Bijection({}) - self.ss_valid_input_kwargs = misc.input_kwarg_list(self._steady_state) + + self.steady_state_options = input_list(self._steady_state) - ['calibration', 'dissolve', 'evaluate_helpers', 'options'] + + standard = ['ss', 'inputs', 'outputs', 'T', 'Js', 'options'] + self.impulse_nonlinear_options = input_list(self._impulse_nonlinear) - standard + self.impulse_linear_options = input_list(self._impulse_linear) - standard + self.jacobian_options = input_list(self._jacobian) - standard + + if isinstance(self, Parent): + self.partial_jacobians_options = input_list(self._partial_jacobians) - standard + + self.ss_valid_input_kwargs = input_kwarg_list(self._steady_state) def inputs(self): pass @@ -28,8 +40,8 @@ def outputs(self): pass def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], - dissolve: Optional[List[str]] = [], evaluate_helpers: bool = False, - **kwargs) -> SteadyStateDict: + dissolve: List[str] = [], evaluate_helpers: bool = False, + options: Dict[str, dict] = {}, **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" # Special handling: 1) Find inputs/outputs of the Block w/o helpers blocks # 2) Add all unknowns of dissolved blocks to inputs @@ -42,40 +54,46 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], inputs |= self.get_attribute(k, 'unknowns').keys() calibration = SteadyStateDict(calibration)[inputs] - kwargs['dissolve'], kwargs['evaluate_helpers'] = dissolve, evaluate_helpers - - return self.M @ self._steady_state(self.M.inv @ calibration, **{k: v for k, v in kwargs.items() if k in self.ss_valid_input_kwargs}) + own_options = self.get_options(options, kwargs, 'steady_state') + if isinstance(self, Parent): + return self.M @ self._steady_state(self.M.inv @ calibration, dissolve=dissolve, evaluate_helpers=evaluate_helpers, options=options, **own_options) + else: + return self.M @ self._steady_state(self.M.inv @ calibration, **own_options) def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Optional[Dict[str, JacobianDict]] = {}, **kwargs) -> ImpulseDict: + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" + own_options = self.get_options(options, kwargs, 'impulse_nonlinear') inputs = ImpulseDict(inputs) _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) # SolvedBlocks may use Js and may be nested in a CombinedBlock if isinstance(self, Parent): - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **kwargs) + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, options, **own_options) else: - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **kwargs) + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **own_options) - def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], - outputs: Optional[List[str]] = None, - Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: + def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" + own_options = self.get_options(options, kwargs, 'impulse_linear') inputs = ImpulseDict(inputs) _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) - return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js) - def partial_jacobians(self, ss: SteadyStateDict, inputs: List[str] = None, outputs: List[str] = None, - T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = {}): + if isinstance(self, Parent): + return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, options, **own_options) + else: + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **own_options) + + def partial_jacobians(self, ss: SteadyStateDict, inputs: Optional[List[str]] = None, outputs: Optional[List[str]] = None, + T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs): if inputs is None: inputs = self.inputs if outputs is None: outputs = self.outputs - inputs, outputs = inputs, outputs # if you have a J for this block that already has everything you need, use it # TODO: add check for T, maybe look at verify_saved_jacobian for ideas? @@ -84,19 +102,22 @@ def partial_jacobians(self, ss: SteadyStateDict, inputs: List[str] = None, outpu # if it's a leaf, just call Jacobian method, include if nonzero if not isinstance(self, Parent): - jac = self.jacobian(ss, inputs, outputs, T) + own_options = self.get_options(options, kwargs, 'jacobian') + jac = self.jacobian(ss, inputs, outputs, T, **own_options) return {self.name: jac} if jac else {} # otherwise call child method with remapping (and remap your own but none of the child Js) - partial = self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T, Js) + own_options = self.get_options(options, kwargs, 'partial_jacobians') + partial = self._partial_jacobians(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T, Js, options, **own_options) if self.name in partial: partial[self.name] = self.M @ partial[self.name] return partial def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, - T: Optional[int] = None, Js: Optional[Dict[str, JacobianDict]] = {}) -> JacobianDict: + T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) + own_options = self.get_options(options, kwargs, 'jacobian') # if you have a J for this block that has everything you need, use it if (self.name in Js) and isinstance(Js[self.name], JacobianDict) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): @@ -105,23 +126,23 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis # if it's a leaf, call Jacobian method, don't supply Js if not isinstance(self, Parent): # TODO should this be remapped? - return self._jacobian(ss, inputs, outputs, T) + return self._jacobian(ss, inputs, outputs, T, **own_options) # otherwise remap own J (currently needed for SolvedBlock only) Js = Js.copy() if self.name in Js: Js[self.name] = self.M.inv @ Js[self.name] - return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js) + return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js, options=options, **own_options) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], - targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: Optional[List] = [], - helper_blocks: Optional[List] = [], helper_targets: Optional[Dict] = {}, - solver: Optional[str] = "", solver_kwargs: Optional[Dict] = {}, - block_kwargs: Optional[Dict] = {}, ttol: Optional[float] = 1e-12, ctol: Optional[float] = 1e-9, - verbose: Optional[bool] = False, check_consistency: Optional[bool] = True, - constrained_method: Optional[str] = "linear_continuation", - constrained_kwargs: Optional[Dict] = {}): + targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: List = [], + helper_blocks: List = [], helper_targets: Dict = {}, + solver: str = "", solver_kwargs: Dict = {}, + ttol: float = 1e-12, ctol: float = 1e-9, + verbose: bool = False, check_consistency: bool = True, + constrained_method: str = "linear_continuation", + constrained_kwargs: Dict = {}, options: Dict[str, dict] = {}, **kwargs): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -142,7 +163,8 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=targets_to_solve, evaluate_helpers=True): ss.update(misc.smart_zip(unknowns_keys, unknown_values)) - ss.update(dag.steady_state(ss, dissolve=dissolve, evaluate_helpers=evaluate_helpers, **block_kwargs)) + kwargs['evaluate_helpers'] = evaluate_helpers + ss.update(dag.steady_state(ss, dissolve=dissolve, options=options, **kwargs)) return compute_target_values(targets, ss) unknowns_solved = solve_for_unknowns(residual, unknowns_to_solve, solver, solver_kwargs, tol=ttol, verbose=verbose, @@ -160,9 +182,9 @@ def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=tar def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Optional[Dict[str, JacobianDict]] = {}, - tol: Optional[Real] = 1E-8, maxit: Optional[int] = 30, - verbose: Optional[bool] = True) -> ImpulseDict: + Js: Dict[str, JacobianDict] = {}, + tol: float = 1E-8, maxit: int = 30, + verbose: bool = True, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -171,14 +193,14 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) T = inputs.T - Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js) - H_U = self.jacobian(ss, unknowns, targets, T, Js) + Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs) H_U_factored = FactoredJacobianDict(H_U, T) # Newton's method U = ImpulseDict({k: np.zeros(T) for k in unknowns}) for it in range(maxit): - results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js=Js) + results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js, options, **kwargs) errors = {k: np.max(np.abs(results[k])) for k in targets} if verbose: print(f'On iteration {it}') @@ -195,7 +217,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Optional[Dict[str, JacobianDict]] = {}) -> ImpulseDict: + Js: Optional[Dict[str, JacobianDict]] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -204,17 +226,17 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) T = inputs.T - Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js) + Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) - H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) - dH = self.impulse_linear(ss, inputs, targets, Js).pack() + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) + dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).pack() dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) - return self.impulse_linear(ss, dU | inputs, outputs, Js) + return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, - Js: Optional[Dict[str, JacobianDict]] = {}) -> JacobianDict: + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -225,15 +247,15 @@ def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) - Js = self.partial_jacobians(ss, inputs | unknowns, (outputs | targets) - unknowns, T, Js) + Js = self.partial_jacobians(ss, inputs | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) - H_U = self.jacobian(ss, unknowns, targets, T, Js).pack(T) - H_Z = self.jacobian(ss, inputs, targets, T, Js).pack(T) + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) + H_Z = self.jacobian(ss, inputs, targets, T, Js, options, **kwargs).pack(T) U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, inputs, T) from sequence_jacobian import combine self_with_unknowns = combine([U_Z, self]) - return self_with_unknowns.jacobian(ss, inputs, unknowns | outputs, T, Js) + return self_with_unknowns.jacobian(ss, inputs, unknowns | outputs, T, Js, options, **kwargs) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: @@ -267,3 +289,18 @@ def default_inputs_outputs(self, ss: SteadyStateDict, inputs, outputs): if outputs is None: outputs = self.outputs - ss._vector_valued() return OrderedSet(inputs), OrderedSet(outputs) + + def get_options(self, options: dict, kwargs, method): + if self.name in options: + options = {**options[self.name], **kwargs} + else: + options = kwargs + + return {k: v for k, v in options.items() if k in self.getattr(method + "_options")} + + solve_jacobian_options = OrderedSet([]) + solve_impulse_linear_options = OrderedSet([]) + solve_impulse_nonlinear_options = OrderedSet(['tol', 'maxit', 'verbose']) + solve_steady_state_options = (input_list(solve_steady_state) - + ['self', 'calibration', 'unknowns', 'targets', 'dissolve', 'helper_blocks', 'helper_targets']) + \ No newline at end of file diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 063a120..82783da 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -60,7 +60,7 @@ def __repr__(self): def _steady_state(self, calibration, dissolve=[], **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" - ss = deepcopy(calibration) + ss = calibration.copy() for block in self.blocks: # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] @@ -69,49 +69,47 @@ def _steady_state(self, calibration, dissolve=[], **kwargs): return ss - def _impulse_nonlinear(self, ss, inputs, outputs, Js): + def _impulse_nonlinear(self, ss, inputs, outputs, Js, options): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() - irf_nonlin_partial_eq = deepcopy(inputs) + irf_nonlin_partial_eq = inputs.copy() for block in self.blocks: input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, Js)) + irf_nonlin_partial_eq.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, Js, options)) return irf_nonlin_partial_eq[original_outputs] - def _impulse_linear(self, ss, inputs, outputs, Js): + def _impulse_linear(self, ss, inputs, outputs, Js, options): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() + #irf_lin_partial_eq = inputs.copy() irf_lin_partial_eq = deepcopy(inputs) for block in self.blocks: input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js)) + irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js, options)) return irf_lin_partial_eq[original_outputs] - def _partial_jacobians(self, ss, inputs, outputs, T, Js): + def _partial_jacobians(self, ss, inputs, outputs, T, Js, options): vector_valued = ss._vector_valued() inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued curlyJs = {} for block in self.blocks: - descendants = block.descendants if isinstance(block, Parent) else {block.name: None} - Js_block = {k: v for k, v in Js.items() if k in descendants} - - curlyJ = block.partial_jacobians(ss, inputs & block.inputs, outputs & block.outputs, T, Js_block) + curlyJ = block.partial_jacobians(ss, inputs & block.inputs, outputs & block.outputs, T, Js, options) curlyJs.update(curlyJ) return curlyJs - def _jacobian(self, ss, inputs, outputs, T, Js={}): - Js = self._partial_jacobians(ss, inputs, outputs, T=T, Js=Js) + def _jacobian(self, ss, inputs, outputs, T, Js, options): + Js = self._partial_jacobians(ss, inputs, outputs, T, Js, options) original_outputs = outputs total_Js = JacobianDict.identity(inputs) @@ -121,9 +119,7 @@ def _jacobian(self, ss, inputs, outputs, T, Js={}): inputs = (inputs | self._required) - vector_valued outputs = (outputs | self._required) - vector_valued for block in self.blocks: - descendants = block.descendants if isinstance(block, Parent) else {block.name: None} - Js_block = {k: v for k, v in Js.items() if k in descendants} - J = block.jacobian(ss, inputs & block.inputs, outputs & block.outputs, T, Js_block) + J = block.jacobian(ss, inputs & block.inputs, outputs & block.outputs, T, Js, options) total_Js.update(J @ total_Js) return total_Js[original_outputs, :] diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 1ced971..5cb96ab 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -134,7 +134,6 @@ def _impulse_linear(self, ss, inputs, outputs, Js): return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) def _jacobian(self, ss, inputs, outputs, T, h=1E-4): - # TODO: h is unusable for now, figure out how to suggest options ss = self.extract_ss_dict(ss) self.update_with_hetinputs(ss) outputs = self.M_outputs.inv @ outputs diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 7c6bfe7..c47e108 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -29,6 +29,10 @@ class SolvedBlock(Block, Parent): def __init__(self, block: Block, name, unknowns, targets, solver=None, solver_kwargs={}): super().__init__() + # since we dispatch to solve methods, same set of options + self.impulse_nonlinear_options = self.solve_impulse_nonlinear_options + self.steady_state_options = self.solve_steady_state_options + self.block = block self.name = name self.unknowns = unknowns @@ -53,42 +57,37 @@ def __init__(self, block: Block, name, unknowns, targets, solver=None, solver_kw def __repr__(self): return f"" - def _steady_state(self, calibration, dissolve=[], unknowns=None, solver="", ttol=1e-9, ctol=1e-9, verbose=False): + def _steady_state(self, calibration, dissolve, options, **kwargs): if self.name in dissolve: - solver = "solved" + kwargs['solver'] = "solved" unknowns = {k: v for k, v in calibration.items() if k in self.unknowns} - - # Allow override of unknowns/solver, if one wants to evaluate the SolvedBlock at a particular set of - # unknown values akin to the steady_state method of Block - if unknowns is None: + else: unknowns = self.unknowns - if not solver: - solver = self.solver + if 'solver' not in kwargs: + # TODO: replace this with default option + kwargs['solver'] = self.solver - return self.block.solve_steady_state(calibration, unknowns, self.targets, solver=solver, - ttol=ttol, ctol=ctol, verbose=verbose) + return self.block.solve_steady_state(calibration, unknowns, self.targets, options, **kwargs) - def _impulse_nonlinear(self, ss, inputs, outputs, Js): + def _impulse_nonlinear(self, ss, inputs, outputs, Js, options, **kwargs): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs - self.unknowns.keys(), Js) + inputs, outputs - self.unknowns.keys(), Js, options, **kwargs) - def _impulse_linear(self, ss, inputs, outputs, Js): + def _impulse_linear(self, ss, inputs, outputs, Js, options): return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs - self.unknowns.keys(), Js) + inputs, outputs - self.unknowns.keys(), Js, options) - def _jacobian(self, ss, inputs, outputs, T, Js): + def _jacobian(self, ss, inputs, outputs, T, Js, options): return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs, T, Js)[outputs] + inputs, outputs, T, Js, options)[outputs] - def _partial_jacobians(self, ss, inputs, outputs, T, Js={}): + def _partial_jacobians(self, ss, inputs, outputs, T, Js, options): # call it on the child first - inner_Js = self.block.partial_jacobians(ss, - inputs=(OrderedSet(self.unknowns) | inputs), - outputs=(OrderedSet(self.targets) | outputs - self.unknowns.keys()), - T=T, Js=Js) + inner_Js = self.block.partial_jacobians(ss, (OrderedSet(self.unknowns) | inputs), + (OrderedSet(self.targets) | outputs - self.unknowns.keys()), T, Js, options) # with these inner Js, also compute H_U and factorize - H_U = self.block.jacobian(ss, inputs=OrderedSet(self.unknowns), outputs=OrderedSet(self.targets), T=T, Js=inner_Js) + H_U = self.block.jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), T, inner_Js, options) H_U_factored = FactoredJacobianDict(H_U, T) return {**inner_Js, self.name: H_U_factored} diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index 774aec2..4591094 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -6,7 +6,7 @@ from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection from .steady_state_dict import SteadyStateDict - +import copy class ImpulseDict: def __init__(self, impulse, T=None): @@ -27,6 +27,11 @@ def __repr__(self): def __iter__(self): return iter(self.impulse) + def copy(self): + newself = copy.copy(self) + newself.impulse = newself.impulse.copy() + return newself + def items(self): return self.impulse.items() diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 53a50b2..79c4f25 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -17,7 +17,7 @@ def make_tuple(x): def input_list(f): """Return list of function inputs (both positional and keyword arguments)""" - return list(inspect.signature(f).parameters) + return OrderedSet(inspect.signature(f).parameters) def input_arg_list(f): @@ -26,7 +26,7 @@ def input_arg_list(f): for p in inspect.signature(f).parameters.values(): if p.default == p.empty: arg_list.append(p.name) - return arg_list + return OrderedSet(arg_list) def input_kwarg_list(f): @@ -35,7 +35,7 @@ def input_kwarg_list(f): for p in inspect.signature(f).parameters.values(): if p.default != p.empty: kwarg_list.append(p.name) - return kwarg_list + return OrderedSet(kwarg_list) def output_list(f): @@ -48,13 +48,13 @@ def output_list(f): Important to write functions in this way when they will be scanned by output_list, for either SimpleBlock or HetBlock. """ - return re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',') + return OrderedSet(re.findall('return (.*?)\n', inspect.getsource(f))[-1].replace(' ', '').split(',')) def metadata(f): name = f.__name__ - inputs = OrderedSet(input_list(f)) - outputs = OrderedSet(output_list(f)) + inputs = input_list(f) + outputs = output_list(f) return name, inputs, outputs diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 0d42c3d..9ac3703 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -2,9 +2,6 @@ import numpy as np -from sequence_jacobian.classes.jacobian_dict import JacobianDict - - def test_ks_jac(krusell_smith_dag): ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag household, firm, mkt_clearing, _, _ = ks_model._blocks_unsorted diff --git a/tests/base/test_options.py b/tests/base/test_options.py new file mode 100644 index 0000000..64c17f6 --- /dev/null +++ b/tests/base/test_options.py @@ -0,0 +1,22 @@ +import numpy as np + +def test_jacobian_h(krusell_smith_dag): + dag, *_, ss = krusell_smith_dag + hh = dag['household'] + + lowacc = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, h=0.05) + midacc = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, h=1E-3) + usual = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, h=1E-4) + nooption = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10) + + assert np.array_equal(usual['C','r'], nooption['C','r']) + assert np.linalg.norm(usual['C','r'] - midacc['C','r']) < np.linalg.norm(usual['C','r'] - lowacc['C','r']) + + midacc_alt = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, options={'household': {'h': 1E-3}}) + assert np.array_equal(midacc['C', 'r'], midacc_alt['C', 'r']) + + lowacc = dag.jacobian(ss, inputs=['K'], outputs=['C'], T=10, options={'household': {'h': 0.05}}) + midacc = dag.jacobian(ss, inputs=['K'], outputs=['C'], T=10, options={'household': {'h': 1E-3}}) + usual = dag.jacobian(ss, inputs=['K'], outputs=['C'], T=10, options={'household': {'h': 1E-4}}) + + assert np.linalg.norm(usual['C','K'] - midacc['C','K']) < np.linalg.norm(usual['C','K'] - lowacc['C','K']) diff --git a/tests/base/test_solved_block.py b/tests/base/test_solved_block.py index 6b1e253..71273f9 100644 --- a/tests/base/test_solved_block.py +++ b/tests/base/test_solved_block.py @@ -15,16 +15,17 @@ def myblock_solved(u, i): res = 0.5 * i(1) - u**2 - u(1) return res +def test_solved_block(): + ss = SteadyStateDict({'u': 5, 'i': 10, 'res': 0.0}) -ss = SteadyStateDict({'u': 5, 'i': 10, 'res': 0.0}) + # Compute jacobian of myblock_solved from scratch + J1 = myblock_solved.jacobian(ss, inputs=['i'], T=20) -# Compute jacobian of myblock_solved from scratch -J1 = myblock_solved.jacobian(ss, inputs=['i'], T=20) + # Compute jacobian of SolvedBlock using a pre-computed FactoredJacobian + J_u = myblock.jacobian(ss, inputs=['u'], T=20) # square jac of underlying simple block + J_factored = FactoredJacobianDict(J_u, T=20) + J_i = myblock.jacobian(ss, inputs=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns + J2 = J_factored.compose(J_i) # obtain jac of unknown wrt to non-unknown inputs using factored jac -# Compute jacobian of SolvedBlock using a pre-computed FactoredJacobian -J_u = myblock.jacobian(ss, inputs=['u'], T=20) # square jac of underlying simple block -J_factored = FactoredJacobianDict(J_u, T=20) -J_i = myblock.jacobian(ss, inputs=['i'], T=20) # jac of underlying simple block wrt inputs that are NOT unknowns -J2 = J_factored.compose(J_i) # obtain jac of unknown wrt to non-unknown inputs using factored jac + assert np.allclose(J1['u']['i'], J2['u']['i']) -assert np.allclose(J1['u']['i'], J2['u']['i']) \ No newline at end of file From dde68fd235c986d6ef683e185e7ae63ad0be0fd7 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 10 Sep 2021 13:52:50 -0500 Subject: [PATCH 266/288] fixed errors, tests now pass, have not tested options beyond jacobian --- src/sequence_jacobian/blocks/block.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index dc25223..6a935a6 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -86,7 +86,7 @@ def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], Im if isinstance(self, Parent): return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, options, **own_options) else: - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **own_options) + return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **own_options) def partial_jacobians(self, ss: SteadyStateDict, inputs: Optional[List[str]] = None, outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs): @@ -138,11 +138,12 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: List = [], helper_blocks: List = [], helper_targets: Dict = {}, + options: Dict[str, dict] = {}, solver: str = "", solver_kwargs: Dict = {}, ttol: float = 1e-12, ctol: float = 1e-9, verbose: bool = False, check_consistency: bool = True, constrained_method: str = "linear_continuation", - constrained_kwargs: Dict = {}, options: Dict[str, dict] = {}, **kwargs): + constrained_kwargs: Dict = {}, **kwargs): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -182,9 +183,9 @@ def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=tar def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Dict[str, JacobianDict] = {}, + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, tol: float = 1E-8, maxit: int = 30, - verbose: bool = True, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + verbose: bool = True, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -296,7 +297,7 @@ def get_options(self, options: dict, kwargs, method): else: options = kwargs - return {k: v for k, v in options.items() if k in self.getattr(method + "_options")} + return {k: v for k, v in options.items() if k in getattr(self, method + "_options")} solve_jacobian_options = OrderedSet([]) solve_impulse_linear_options = OrderedSet([]) From cd479ac357c1b1f25e1c5628b5a758df100c8a46 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 10 Sep 2021 16:21:02 -0500 Subject: [PATCH 267/288] options seems debugged, but not tested extensively --- .../auxiliary_blocks/calibration_block.py | 4 +- .../auxiliary_blocks/jacobiandict_block.py | 1 + src/sequence_jacobian/blocks/block.py | 103 +++++++++--------- .../blocks/combined_block.py | 2 +- src/sequence_jacobian/utilities/function.py | 18 +-- src/sequence_jacobian/utilities/misc.py | 2 +- tests/base/test_options.py | 38 +++++++ tests/base/test_workflow.py | 2 - 8 files changed, 102 insertions(+), 68 deletions(-) diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py index 5371f2b..3cff60e 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py @@ -9,6 +9,8 @@ class CalibrationBlock(CombinedBlock): """A CalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an CalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" + i_am_calibration_block = True + def __init__(self, blocks, helper_blocks, calibration, name=""): sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) @@ -24,7 +26,7 @@ def __init__(self, blocks, helper_blocks, calibration, name=""): def __repr__(self): return f"" - def _steady_state(self, calibration, dissolve=[], helper_targets={}, evaluate_helpers=True, **block_kwargs): + def _steady_state(self, calibration, dissolve, helper_targets, evaluate_helpers, **block_kwargs): """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" ss = calibration.copy() helper_outputs = {} diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py index a0e8f58..03fbddc 100644 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py +++ b/src/sequence_jacobian/blocks/auxiliary_blocks/jacobiandict_block.py @@ -7,6 +7,7 @@ class JacobianDictBlock(JacobianDict, Block): """A wrapper for nested dicts/JacobianDicts passed directly into DAGs to ensure method compatibility""" def __init__(self, nesteddict, outputs=None, inputs=None, name=None): super().__init__(nesteddict, outputs=outputs, inputs=inputs, name=name) + Block.__init__(self) def __repr__(self): return f"" diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 6a935a6..fd28968 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -8,7 +8,7 @@ from .support.steady_state import provide_solver_default, solve_for_unknowns, compute_target_values from .support.parent import Parent from ..utilities import misc -from ..utilities.function import input_list, input_kwarg_list +from ..utilities.function import input_defaults from ..utilities.bijection import Bijection from ..utilities.ordered_set import OrderedSet from ..classes import SteadyStateDict, UserProvidedSS, ImpulseDict, JacobianDict, FactoredJacobianDict @@ -21,18 +21,12 @@ class Block: def __init__(self): self.M = Bijection({}) - self.steady_state_options = input_list(self._steady_state) - ['calibration', 'dissolve', 'evaluate_helpers', 'options'] - - standard = ['ss', 'inputs', 'outputs', 'T', 'Js', 'options'] - self.impulse_nonlinear_options = input_list(self._impulse_nonlinear) - standard - self.impulse_linear_options = input_list(self._impulse_linear) - standard - self.jacobian_options = input_list(self._jacobian) - standard - - if isinstance(self, Parent): - self.partial_jacobians_options = input_list(self._partial_jacobians) - standard + self.steady_state_options = self.input_defaults_smart('_steady_state') + self.impulse_nonlinear_options = self.input_defaults_smart('_impulse_nonlinear') + self.impulse_linear_options = self.input_defaults_smart('_impulse_linear') + self.jacobian_options = self.input_defaults_smart('_jacobian') + self.partial_jacobians_options = self.input_defaults_smart('_partial_jacobians') - self.ss_valid_input_kwargs = input_kwarg_list(self._steady_state) - def inputs(self): pass @@ -41,7 +35,7 @@ def outputs(self): def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], dissolve: List[str] = [], evaluate_helpers: bool = False, - options: Dict[str, dict] = {}, **kwargs) -> SteadyStateDict: + helper_targets: dict = {}, options: Dict[str, dict] = {}, **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" # Special handling: 1) Find inputs/outputs of the Block w/o helpers blocks # 2) Add all unknowns of dissolved blocks to inputs @@ -56,7 +50,10 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], calibration = SteadyStateDict(calibration)[inputs] own_options = self.get_options(options, kwargs, 'steady_state') if isinstance(self, Parent): - return self.M @ self._steady_state(self.M.inv @ calibration, dissolve=dissolve, evaluate_helpers=evaluate_helpers, options=options, **own_options) + if hasattr(self, 'i_am_calibration_block'): + own_options['evaluate_helpers'] = evaluate_helpers + own_options['helper_targets'] = helper_targets + return self.M @ self._steady_state(self.M.inv @ calibration, dissolve=dissolve, options=options, **own_options) else: return self.M @ self._steady_state(self.M.inv @ calibration, **own_options) @@ -125,8 +122,7 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis # if it's a leaf, call Jacobian method, don't supply Js if not isinstance(self, Parent): - # TODO should this be remapped? - return self._jacobian(ss, inputs, outputs, T, **own_options) + return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T, **own_options) # otherwise remap own J (currently needed for SolvedBlock only) Js = Js.copy() @@ -134,21 +130,21 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis Js[self.name] = self.M.inv @ Js[self.name] return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js, options=options, **own_options) + solve_steady_state_options = dict(solver="", solver_kwargs={}, ttol=1e-12, ctol=1e-9, verbose=False, + check_consistency=True, constrained_method="linear_continuation", constrained_kwargs={}) + def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: List = [], helper_blocks: List = [], helper_targets: Dict = {}, - options: Dict[str, dict] = {}, - solver: str = "", solver_kwargs: Dict = {}, - ttol: float = 1e-12, ctol: float = 1e-9, - verbose: bool = False, check_consistency: bool = True, - constrained_method: str = "linear_continuation", - constrained_kwargs: Dict = {}, **kwargs): + options: Dict[str, dict] = {}, **kwargs): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - if helper_blocks is not None: - if helper_targets is None: + opts = self.get_options(options, kwargs, 'solve_steady_state') + + if helper_blocks: + if not helper_targets: raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" " in the `helper_targets` keyword argument.") else: @@ -159,33 +155,32 @@ def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], else: dag, ss, unknowns_to_solve, targets_to_solve = self, SteadyStateDict(calibration), unknowns, targets - solver = solver if solver else provide_solver_default(unknowns) + solver = opts['solver'] if opts['solver'] else provide_solver_default(unknowns) def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=targets_to_solve, evaluate_helpers=True): ss.update(misc.smart_zip(unknowns_keys, unknown_values)) - kwargs['evaluate_helpers'] = evaluate_helpers - ss.update(dag.steady_state(ss, dissolve=dissolve, options=options, **kwargs)) + ss.update(dag.steady_state(ss, dissolve=dissolve, options=options, evaluate_helpers=evaluate_helpers, **kwargs)) return compute_target_values(targets, ss) - unknowns_solved = solve_for_unknowns(residual, unknowns_to_solve, solver, solver_kwargs, tol=ttol, verbose=verbose, - constrained_method=constrained_method, constrained_kwargs=constrained_kwargs) + unknowns_solved = solve_for_unknowns(residual, unknowns_to_solve, solver, opts['solver_kwargs'], tol=opts['ttol'], verbose=opts['verbose'], + constrained_method=opts['constrained_method'], constrained_kwargs=opts['constrained_kwargs']) - if helper_blocks and helper_targets and check_consistency: + if helper_blocks and helper_targets and opts['check_consistency']: # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) cresid = np.max(abs(residual(unknowns_solved.values(), unknowns_keys=unknowns_solved.keys(), targets=targets, evaluate_helpers=False))) - if cresid > ctol: + if cresid > opts['ctol']: raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" f" the consistency of the DAG without redirection.") return ss + solve_impulse_nonlinear_options = dict(tol=1E-8, maxit=30, verbose=True) + def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, - tol: float = 1E-8, maxit: int = 30, - verbose: bool = True, **kwargs) -> ImpulseDict: + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -198,24 +193,30 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs) H_U_factored = FactoredJacobianDict(H_U, T) + opts = self.get_options(options, kwargs, 'solve_impulse_nonlinear') + # Newton's method U = ImpulseDict({k: np.zeros(T) for k in unknowns}) - for it in range(maxit): + if opts['verbose']: + print(f'Solving {self.name} for {unknowns} to hit {targets}') + for it in range(opts['maxit']): results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js, options, **kwargs) errors = {k: np.max(np.abs(results[k])) for k in targets} - if verbose: + if opts['verbose']: print(f'On iteration {it}') for k in errors: print(f' max error for {k} is {errors[k]:.2E}') - if all(v < tol for v in errors.values()): + if all(v < opts['tol'] for v in errors.values()): break else: U += H_U_factored.apply(results) else: - raise ValueError(f'No convergence after {maxit} backward iterations!') + raise ValueError(f'No convergence after {opts["maxit"]} backward iterations!') return results | U + solve_impulse_linear_options = {} + def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js: Optional[Dict[str, JacobianDict]] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: @@ -235,6 +236,8 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) + solve_jacobian_options = {} + def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: @@ -292,16 +295,18 @@ def default_inputs_outputs(self, ss: SteadyStateDict, inputs, outputs): return OrderedSet(inputs), OrderedSet(outputs) def get_options(self, options: dict, kwargs, method): + own_options = getattr(self, method + "_options") + if self.name in options: - options = {**options[self.name], **kwargs} + merged = {**own_options, **options[self.name], **kwargs} else: - options = kwargs - - return {k: v for k, v in options.items() if k in getattr(self, method + "_options")} - - solve_jacobian_options = OrderedSet([]) - solve_impulse_linear_options = OrderedSet([]) - solve_impulse_nonlinear_options = OrderedSet(['tol', 'maxit', 'verbose']) - solve_steady_state_options = (input_list(solve_steady_state) - - ['self', 'calibration', 'unknowns', 'targets', 'dissolve', 'helper_blocks', 'helper_targets']) - \ No newline at end of file + merged = {**own_options, **kwargs} + + return {k: merged[k] for k in own_options} + + def input_defaults_smart(self, methodname): + method = getattr(self, methodname, None) + if method is None: + return {} + else: + return input_defaults(method) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 82783da..eb73b15 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -57,7 +57,7 @@ def __repr__(self): else: return f"" - def _steady_state(self, calibration, dissolve=[], **kwargs): + def _steady_state(self, calibration, dissolve, **kwargs): """Evaluate a partial equilibrium steady state of the CombinedBlock given a `calibration`""" ss = calibration.copy() diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 79c4f25..a142ed8 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -20,22 +20,12 @@ def input_list(f): return OrderedSet(inspect.signature(f).parameters) -def input_arg_list(f): - """Return list of function positional arguments *only*""" - arg_list = [] - for p in inspect.signature(f).parameters.values(): - if p.default == p.empty: - arg_list.append(p.name) - return OrderedSet(arg_list) - - -def input_kwarg_list(f): - """Return list of function keyword arguments *only*""" - kwarg_list = [] +def input_defaults(f): + defaults = {} for p in inspect.signature(f).parameters.values(): if p.default != p.empty: - kwarg_list.append(p.name) - return OrderedSet(kwarg_list) + defaults[p.name] = p.default + return defaults def output_list(f): diff --git a/src/sequence_jacobian/utilities/misc.py b/src/sequence_jacobian/utilities/misc.py index cc03ea8..7c7964c 100644 --- a/src/sequence_jacobian/utilities/misc.py +++ b/src/sequence_jacobian/utilities/misc.py @@ -140,4 +140,4 @@ def logsum(vfun, lam): return VE -from .function import (input_list, input_arg_list, input_kwarg_list, output_list) +#from .function import (input_list, output_list) diff --git a/tests/base/test_options.py b/tests/base/test_options.py index 64c17f6..5cac169 100644 --- a/tests/base/test_options.py +++ b/tests/base/test_options.py @@ -1,4 +1,6 @@ import numpy as np +import pytest +from sequence_jacobian.models import krusell_smith def test_jacobian_h(krusell_smith_dag): dag, *_, ss = krusell_smith_dag @@ -20,3 +22,39 @@ def test_jacobian_h(krusell_smith_dag): usual = dag.jacobian(ss, inputs=['K'], outputs=['C'], T=10, options={'household': {'h': 1E-4}}) assert np.linalg.norm(usual['C','K'] - midacc['C','K']) < np.linalg.norm(usual['C','K'] - lowacc['C','K']) + + +def test_jacobian_steady_state(krusell_smith_dag): + dag = krusell_smith_dag[0] + calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, + "nS": 2, "nA": 10, "amax": 200, "r": 0.01, 'beta': 0.96, "Z": 0.85, "K": 3.} + + pytest.raises(ValueError, dag.steady_state, calibration, options={'household': {'backward_maxit': 10}}) + + ss1 = dag.steady_state(calibration) + ss2 = dag.steady_state(calibration, options={'household': {'backward_maxit': 100000}}) + assert ss1['A'] == ss2['A'] + + + +def test_steady_state_solution(krusell_smith_dag): + dag, _, unknowns, targets, _ = krusell_smith_dag + helper_blocks = [krusell_smith.firm_steady_state_solution] + + calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, + "nS": 2, "nA": 10, "amax": 200, "r": 0.01} + unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} + targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} + + pytest.raises(RuntimeError, dag.solve_steady_state, calibration, unknowns_ss, targets_ss, solver="brentq", + helper_blocks=helper_blocks, helper_targets=["Y", "r"], ttol=1E-2, ctol=1E-9) + + # ss = dag.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", + # helper_blocks=helper_blocks, helper_targets=["Y", "r"], verbose=False) + + # assert not capsys.readouterr().out + + # ss = dag.solve_steady_state(calibration, {**unknowns_ss, 'beta':0.98}, targets_ss, solver="newton_custom", + # helper_blocks=helper_blocks, helper_targets=["Y", "r"]) + + # assert capsys.readouterr().out diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 143b2c9..35c1a49 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -150,5 +150,3 @@ def test_all(): inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') - -test_all() From d555b2f506851275cf721ca3a8870f1bb28e1b5a Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 14 Sep 2021 12:20:35 -0500 Subject: [PATCH 268/288] factored out common functionality of SteadyStateDict and ImpulseDict into ResultDict; ImpulseDict therefore now has internal --- src/sequence_jacobian/classes/impulse_dict.py | 103 +++++------------- src/sequence_jacobian/classes/result_dict.py | 78 +++++++++++++ .../classes/steady_state_dict.py | 71 +----------- tests/base/test_public_classes.py | 2 +- tests/base/test_simple_block.py | 6 +- tests/base/test_workflow.py | 3 + 6 files changed, 113 insertions(+), 150 deletions(-) create mode 100644 src/sequence_jacobian/classes/result_dict.py diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index 4591094..62fb512 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -1,122 +1,71 @@ """ImpulseDict class for manipulating impulse responses.""" import numpy as np -from copy import deepcopy + +from .result_dict import ResultDict from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection from .steady_state_dict import SteadyStateDict -import copy -class ImpulseDict: - def __init__(self, impulse, T=None): - if isinstance(impulse, ImpulseDict): - self.impulse = impulse.impulse - self.T = impulse.T +class ImpulseDict(ResultDict): + def __init__(self, data, internal=None, T=None): + if isinstance(data, ImpulseDict): + if internal is not None or T is not None: + raise ValueError('Supplying ImpulseDict and also internal or T to constructor not allowed') + super().__init__(data) + self.T = data.T else: - if not isinstance(impulse, dict): - raise ValueError('ImpulseDicts are initialized with a `dict` of impulse responses.') - self.impulse = impulse - if T is None: - T = self.infer_length() - self.T = T - - def __repr__(self): - return f'' - - def __iter__(self): - return iter(self.impulse) - - def copy(self): - newself = copy.copy(self) - newself.impulse = newself.impulse.copy() - return newself - - def items(self): - return self.impulse.items() - - def update(self, other): - return self.impulse.update(other.impulse) - - def __or__(self, other): - if not isinstance(other, ImpulseDict): - raise ValueError('Trying to merge an ImpulseDict with something else.') - # Union returns a new ImpulseDict - merged = type(self)(self.impulse) - merged.impulse.update(other.impulse) - return merged - - def __getitem__(self, item): - # Behavior similar to pandas - if isinstance(item, str): - # Case 1: ImpulseDict['C'] returns array - return self.impulse[item] - elif isinstance(item, list) or isinstance(item, OrderedSet): - # Case 2: ImpulseDict[['C']] or ImpulseDict[['C', 'Y']] return smaller ImpulseDicts - return type(self)({k: self.impulse[k] for k in item}) - else: - ValueError("Use ImpulseDict['X'] to return an array or ImpulseDict[['X']] to return a smaller ImpulseDict.") + if not isinstance(data, dict): + raise ValueError('ImpulseDicts are initialized with a `dict` of top-level impulse responses.') + super().__init__(data, internal) + self.T = (T if T is not None else self.infer_length()) def __add__(self, other): if isinstance(other, (float, int)): - return type(self)({k: v + other for k, v in self.impulse.items()}) + return type(self)({k: v + other for k, v in self.toplevel.items()}) elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v + other[k] for k, v in self.impulse.items()}) + return type(self)({k: v + other[k] for k, v in self.toplevel.items()}) else: return NotImplementedError('Only a number or a SteadyStateDict can be added from an ImpulseDict.') def __sub__(self, other): if isinstance(other, (float, int)): - return type(self)({k: v - other for k, v in self.impulse.items()}) + return type(self)({k: v - other for k, v in self.toplevel.items()}) elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v - other[k] for k, v in self.impulse.items()}) + return type(self)({k: v - other[k] for k, v in self.toplevel.items()}) else: return NotImplementedError('Only a number or a SteadyStateDict can be subtracted from an ImpulseDict.') def __mul__(self, other): if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.impulse.items()}) + return type(self)({k: v * other for k, v in self.toplevel.items()}) elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + return type(self)({k: v * other[k] for k, v in self.toplevel.items()}) else: return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __rmul__(self, other): if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.impulse.items()}) + return type(self)({k: v * other for k, v in self.toplevel.items()}) elif isinstance(other, SteadyStateDict): - return type(self)({k: v * other[k] for k, v in self.impulse.items()}) + return type(self)({k: v * other[k] for k, v in self.toplevel.items()}) else: return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') def __truediv__(self, other): if isinstance(other, (float, int)): - return type(self)({k: v / other for k, v in self.impulse.items()}) + return type(self)({k: v / other for k, v in self.toplevel.items()}) # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero elif isinstance(other, SteadyStateDict): - return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.impulse.items()}) + return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.toplevel.items()}) else: return NotImplementedError('An ImpulseDict can only be divided by a number or a SteadyStateDict.') - def __matmul__(self, x): - # remap keys in toplevel - if isinstance(x, Bijection): - new = deepcopy(self) - new.impulse = x @ self.impulse - return new - else: - return NotImplemented - - def __rmatmul__(self, x): - return self.__matmul__(x) - - def keys(self): - return self.impulse.keys() - def pack(self): T = self.T - bigv = np.empty(T*len(self.impulse)) - for i, v in enumerate(self.impulse.values()): + bigv = np.empty(T*len(self.toplevel)) + for i, v in enumerate(self.toplevel.values()): bigv[i*T:(i+1)*T] = v return bigv @@ -128,7 +77,7 @@ def unpack(bigv, outputs, T): return ImpulseDict(impulse) def infer_length(self): - lengths = [len(v) for v in self.impulse.values()] + lengths = [len(v) for v in self.toplevel.values()] length = max(lengths) if length != min(lengths): raise ValueError(f'Building ImpulseDict with inconsistent lengths {max(lengths)} and {min(lengths)}') diff --git a/src/sequence_jacobian/classes/result_dict.py b/src/sequence_jacobian/classes/result_dict.py new file mode 100644 index 0000000..8cf864f --- /dev/null +++ b/src/sequence_jacobian/classes/result_dict.py @@ -0,0 +1,78 @@ +import copy + +from ..utilities.bijection import Bijection + +class ResultDict: + def __init__(self, data, internal=None): + if isinstance(data, ResultDict): + if internal is not None: + raise ValueError(f'Supplying {type(self).__name__} and also internal to constructor not allowed') + self.toplevel = data.toplevel.copy() + self.internal = data.internal.copy() + else: + self.toplevel: dict = data.copy() + self.internal: dict = {} if internal is None else internal.copy() + + def __repr__(self): + if self.internal: + return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" + else: + return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" + + def __iter__(self): + return iter(self.toplevel) + + def __getitem__(self, k): + if isinstance(k, str): + return self.toplevel[k] + elif isinstance(k, tuple): + raise TypeError(f'Key {k} to {type(self).__name__} cannot be tuple') + else: + try: + return type(self)({ki: self.toplevel[ki] for ki in k}) + except TypeError: + raise TypeError(f'Key {k} to {type(self).__name__} needs to be a string or an iterable (list, set, etc) of strings') + + def __setitem__(self, k, v): + self.toplevel[k] = v + + def __matmul__(self, x): + # remap keys in toplevel + if isinstance(x, Bijection): + new = copy.deepcopy(self) + new.toplevel = x @ self.toplevel + return new + else: + return NotImplemented + + def __rmatmul__(self, x): + return self.__matmul__(x) + + def __len__(self): + return len(self.toplevel) + + def __or__(self, other): + if not isinstance(other, type(self)): + raise ValueError(f'Trying to merge a {type(self).__name__} with a {type(other).__name__}.') + merged = self.copy() + merged.update(other) + return merged + + def keys(self): + return self.toplevel.keys() + + def values(self): + return self.toplevel.values() + + def items(self): + return self.toplevel.items() + + def update(self, rdict): + if isinstance(rdict, ResultDict): + self.toplevel.update(rdict.toplevel) + self.internal.update(rdict.internal) + else: + self.toplevel.update(dict(rdict)) + + def copy(self): + return type(self)(self) diff --git a/src/sequence_jacobian/classes/steady_state_dict.py b/src/sequence_jacobian/classes/steady_state_dict.py index 4767e3a..b994606 100644 --- a/src/sequence_jacobian/classes/steady_state_dict.py +++ b/src/sequence_jacobian/classes/steady_state_dict.py @@ -1,5 +1,6 @@ from copy import deepcopy +from .result_dict import ResultDict from ..utilities.misc import dict_diff from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection @@ -10,78 +11,10 @@ from typing import Any, Dict, Union Array = Any -class SteadyStateDict: - # TODO: should this just subclass dict so we can avoid a lot of boilerplate? - # Really this is just a top-level dict (with all the usual functionality) with "internal" bolted on - - def __init__(self, data, internal=None): - if isinstance(data, SteadyStateDict): - if internal is not None: - raise ValueError('Supplying SteadyStateDict and also internal to constructor not allowed') - self.toplevel = data.toplevel.copy() - self.internal = data.internal.copy() - else: - self.toplevel: dict = data.copy() - self.internal: dict = {} if internal is None else internal.copy() - - def __repr__(self): - if self.internal: - return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" - else: - return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" - - def __iter__(self): - return iter(self.toplevel) - - def __getitem__(self, k): - if isinstance(k, str): - return self.toplevel[k] - else: - try: - return SteadyStateDict({ki: self.toplevel[ki] for ki in k}) - except TypeError: - raise TypeError(f'Key {k} needs to be a string or an iterable (list, set, etc) of strings') - - def __setitem__(self, k, v): - self.toplevel[k] = v - - def __matmul__(self, x): - # remap keys in toplevel - if isinstance(x, Bijection): - new = deepcopy(self) - new.toplevel = x @ self.toplevel - return new - else: - return NotImplemented - - def __rmatmul__(self, x): - return self.__matmul__(x) - - def __len__(self): - return len(self.toplevel) - - def keys(self): - return self.toplevel.keys() - - def values(self): - return self.toplevel.values() - - def items(self): - return self.toplevel.items() - - def update(self, ssdict): - if isinstance(ssdict, SteadyStateDict): - self.toplevel.update(ssdict.toplevel) - self.internal.update(ssdict.internal) - else: - self.toplevel.update(dict(ssdict)) - +class SteadyStateDict(ResultDict): def difference(self, data_to_remove): return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internal)) - def copy(self): - return SteadyStateDict(self) - def _vector_valued(self): return OrderedSet([k for k, v in self.toplevel.items() if np.size(v) > 1]) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index 0b3c5d9..ad47703 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -22,7 +22,7 @@ def test_impulsedict(krusell_smith_dag): # Merge method temp = ir_lin[['C', 'K']] | ir_lin[['r']] - assert list(temp.impulse.keys()) == ['C', 'K', 'r'] + assert list(temp.keys()) == ['C', 'K', 'r'] # SS and scalar multiplication dC1 = 100 * ir_lin['C'] / ss['C'] diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index 16ab5a8..d01329a 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -42,7 +42,7 @@ def test_block_consistency(block, ss): # now if we put in constant inputs, td should give us the same! td_results = block.impulse_nonlinear(ss_results, {k: np.zeros(20) for k in ss.keys()}) - for v in td_results.impulse.values(): + for v in td_results.values(): assert np.all(v == 0) # now get the Jacobian @@ -57,8 +57,8 @@ def test_block_consistency(block, ss): td_up = block.impulse_nonlinear(ss_results, {i: h*shock for i, shock in all_shocks.items()}) td_dn = block.impulse_nonlinear(ss_results, {i: -h*shock for i, shock in all_shocks.items()}) - linear_impulses = {o: (td_up.impulse[o] - td_dn.impulse[o])/(2*h) for o in td_up.impulse} - linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up.impulse} + linear_impulses = {o: (td_up[o] - td_dn[o])/(2*h) for o in td_up} + linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up} for o in linear_impulses: assert np.all(np.abs(linear_impulses[o] - linear_impulses_from_jac[o]) < 1E-5) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 35c1a49..9df7e8f 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -150,3 +150,6 @@ def test_all(): inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') + + +test_all() From 3a0e00d2b849fac07d126f9412a0305d41e959c1 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 14 Sep 2021 12:46:55 -0500 Subject: [PATCH 269/288] changed "internal" to "internals" everywhere, preparing for internals= to be output keyword --- src/sequence_jacobian/blocks/het_block.py | 20 +++++++++---------- src/sequence_jacobian/classes/impulse_dict.py | 6 +++--- src/sequence_jacobian/classes/result_dict.py | 16 +++++++-------- .../classes/steady_state_dict.py | 2 +- src/sequence_jacobian/models/two_asset.py | 6 +++--- tests/base/test_multiexog.py | 4 ++-- tests/base/test_public_classes.py | 2 +- tests/base/test_workflow.py | 2 -- 8 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 5cb96ab..52bc82a 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -31,12 +31,12 @@ def __init__(self, backward_fun, exogenous, policy, backward, backward_init=None self.M_outputs = Bijection({o: o.upper() for o in self.non_backward_outputs}) self.inputs = self.backward_fun.inputs - [k + '_p' for k in self.backward] self.inputs |= self.exogenous - self.internal = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.backward_fun.outputs + self.internals = OrderedSet(['D', 'Dbeg']) | self.exogenous | self.backward_fun.outputs # store "original" copies of these for use whenever we process new hetinputs/hetoutputs self.original_inputs = self.inputs self.original_outputs = self.outputs - self.original_internal = self.internal + self.original_internals = self.internals self.original_M_outputs = self.M_outputs # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. @@ -103,8 +103,8 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, aggregates = {o.upper(): np.vdot(D, ss[o]) for o in toreturn} ss.update(aggregates) - return SteadyStateDict({k: ss[k] for k in ss if k not in self.internal}, - {self.name: {k: ss[k] for k in ss if k in self.internal}}) + return SteadyStateDict({k: ss[k] for k in ss if k not in self.internals}, + {self.name: {k: ss[k] for k in ss if k in self.internals}}) def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindividual=False): ss = self.extract_ss_dict(ssin) @@ -415,22 +415,22 @@ def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunct self = copy.copy(self) inputs = self.original_inputs.copy() outputs = self.original_outputs.copy() - internal = self.original_internal.copy() + internals = self.original_internals.copy() if hetoutputs is not None: inputs |= (hetoutputs.inputs - self.backward_fun.outputs - ['D']) outputs |= [o.upper() for o in hetoutputs.outputs] self.M_outputs = Bijection({o: o.upper() for o in hetoutputs.outputs}) @ self.original_M_outputs - internal |= hetoutputs.outputs + internals |= hetoutputs.outputs if hetinputs is not None: inputs |= hetinputs.inputs inputs -= hetinputs.outputs - internal |= hetinputs.outputs + internals |= hetinputs.outputs self.inputs = inputs self.outputs = outputs - self.internal = internal + self.internals = internals self.hetinputs = hetinputs self.hetoutputs = hetoutputs @@ -468,8 +468,8 @@ def update_with_hetoutputs(self, d): def extract_ss_dict(self, ss): if isinstance(ss, SteadyStateDict): ssnew = ss.toplevel.copy() - if self.name in ss.internal: - ssnew.update(ss.internal[self.name]) + if self.name in ss.internals: + ssnew.update(ss.internals[self.name]) return ssnew else: return ss.copy() diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index 62fb512..2a82ab4 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -9,16 +9,16 @@ from .steady_state_dict import SteadyStateDict class ImpulseDict(ResultDict): - def __init__(self, data, internal=None, T=None): + def __init__(self, data, internals=None, T=None): if isinstance(data, ImpulseDict): - if internal is not None or T is not None: + if internals is not None or T is not None: raise ValueError('Supplying ImpulseDict and also internal or T to constructor not allowed') super().__init__(data) self.T = data.T else: if not isinstance(data, dict): raise ValueError('ImpulseDicts are initialized with a `dict` of top-level impulse responses.') - super().__init__(data, internal) + super().__init__(data, internals) self.T = (T if T is not None else self.infer_length()) def __add__(self, other): diff --git a/src/sequence_jacobian/classes/result_dict.py b/src/sequence_jacobian/classes/result_dict.py index 8cf864f..834e880 100644 --- a/src/sequence_jacobian/classes/result_dict.py +++ b/src/sequence_jacobian/classes/result_dict.py @@ -3,19 +3,19 @@ from ..utilities.bijection import Bijection class ResultDict: - def __init__(self, data, internal=None): + def __init__(self, data, internals=None): if isinstance(data, ResultDict): - if internal is not None: - raise ValueError(f'Supplying {type(self).__name__} and also internal to constructor not allowed') + if internals is not None: + raise ValueError(f'Supplying {type(self).__name__} and also internals to constructor not allowed') self.toplevel = data.toplevel.copy() - self.internal = data.internal.copy() + self.internals = data.internals.copy() else: self.toplevel: dict = data.copy() - self.internal: dict = {} if internal is None else internal.copy() + self.internals: dict = {} if internals is None else internals.copy() def __repr__(self): - if self.internal: - return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internal={list(self.internal.keys())}>" + if self.internals: + return f"<{type(self).__name__}: {list(self.toplevel.keys())}, internals={list(self.internals.keys())}>" else: return f"<{type(self).__name__}: {list(self.toplevel.keys())}>" @@ -70,7 +70,7 @@ def items(self): def update(self, rdict): if isinstance(rdict, ResultDict): self.toplevel.update(rdict.toplevel) - self.internal.update(rdict.internal) + self.internals.update(rdict.internals) else: self.toplevel.update(dict(rdict)) diff --git a/src/sequence_jacobian/classes/steady_state_dict.py b/src/sequence_jacobian/classes/steady_state_dict.py index b994606..343bcaf 100644 --- a/src/sequence_jacobian/classes/steady_state_dict.py +++ b/src/sequence_jacobian/classes/steady_state_dict.py @@ -13,7 +13,7 @@ class SteadyStateDict(ResultDict): def difference(self, data_to_remove): - return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internal)) + return SteadyStateDict(dict_diff(self.toplevel, data_to_remove), deepcopy(self.internals)) def _vector_valued(self): return OrderedSet([k for k, v in self.toplevel.items() if np.size(v) > 1]) diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py index 3dc9714..14f12ac 100644 --- a/src/sequence_jacobian/models/two_asset.py +++ b/src/sequence_jacobian/models/two_asset.py @@ -386,11 +386,11 @@ def res(x): pshare = p / (tot_wealth - Bh) # calculate aggregate adjustment cost and check Walras's law - chi = get_Psi_and_deriv(ss.internal["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] - Chi = np.vdot(ss.internal["household"]['D'], chi) + chi = get_Psi_and_deriv(ss.internals["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] + Chi = np.vdot(ss.internals["household"]['D'], chi) goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 - ss.internal["household"].update({"chi": chi}) + ss.internals["household"].update({"chi": chi}) ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'CHI': Chi, 'phi': phi, 'wealth': tot_wealth, 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, diff --git a/tests/base/test_multiexog.py b/tests/base/test_multiexog.py index 674523a..9e29e30 100644 --- a/tests/base/test_multiexog.py +++ b/tests/base/test_multiexog.py @@ -69,8 +69,8 @@ def test_equivalence(): assert np.isclose(ss_multidim['A'], ss_onedim['A']) and np.isclose(ss_multidim['C'], ss_onedim['C']) - D_onedim = ss_onedim.internal['household_onedim']['D'] - D_multidim = ss_multidim.internal['household_multidim']['D'] + D_onedim = ss_onedim.internals['household_onedim']['D'] + D_multidim = ss_multidim.internals['household_multidim']['D'] assert np.allclose(D_onedim, D_multidim.reshape(*D_onedim.shape)) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index ad47703..d7ca66c 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -46,7 +46,7 @@ def test_bijection(): assert (mymap2 @ mymap)['a'] == 'a2' # composition with SteadyStateDict - ss = SteadyStateDict({'a': 2.0, 'b': 1.0}, internal={}) + ss = SteadyStateDict({'a': 2.0, 'b': 1.0}) ss_remapped = ss @ mymap assert isinstance(ss_remapped, SteadyStateDict) assert ss_remapped['a1'] == ss['a'] and ss_remapped['b1'] == ss['b'] diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 9df7e8f..c7ee0d8 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -151,5 +151,3 @@ def test_all(): assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') - -test_all() From 07a8bf882a14b1a512ef7b47b8ea7bb924a5742a Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 14 Sep 2021 14:39:05 -0500 Subject: [PATCH 270/288] impulse_nonlinear can now return internals --- src/sequence_jacobian/blocks/block.py | 22 +++++- .../blocks/combined_block.py | 12 +-- src/sequence_jacobian/blocks/het_block.py | 25 +++--- src/sequence_jacobian/blocks/solved_block.py | 4 +- src/sequence_jacobian/classes/impulse_dict.py | 76 +++++++++++-------- src/sequence_jacobian/utilities/bijection.py | 8 +- tests/base/test_workflow.py | 4 +- 7 files changed, 93 insertions(+), 58 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index fd28968..3352858 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -58,7 +58,7 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], return self.M @ self._steady_state(self.M.inv @ calibration, **own_options) def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], - outputs: Optional[List[str]] = None, + outputs: Optional[List[str]] = None, internals: Union[Dict[str, List[str]], List[str]] = {}, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" @@ -68,7 +68,9 @@ def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], # SolvedBlocks may use Js and may be nested in a CombinedBlock if isinstance(self, Parent): - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, options, **own_options) + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, internals, Js, options, **own_options) + elif hasattr(self, 'internals'): + return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, self.internals_to_report(internals), **own_options) else: return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **own_options) @@ -180,7 +182,8 @@ def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=tar def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + internals: Union[Dict[str, List[str]], List[str]] = {}, Js: Dict[str, JacobianDict] = {}, + options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -200,7 +203,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ if opts['verbose']: print(f'Solving {self.name} for {unknowns} to hit {targets}') for it in range(opts['maxit']): - results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, Js, options, **kwargs) + results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, internals, Js, options, **kwargs) errors = {k: np.max(np.abs(results[k])) for k in targets} if opts['verbose']: print(f'On iteration {it}') @@ -310,3 +313,14 @@ def input_defaults_smart(self, methodname): return {} else: return input_defaults(method) + + def internals_to_report(self, internals): + if self.name in internals: + if isinstance(internals, dict): + # if internals is a dict, we've specified which internals we want from each block + return internals[self.name] + else: + # otherwise internals is some kind of iterable or set, and if we're in it, we want everything + return self.internals + else: + return [] diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index eb73b15..3ba41f0 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -5,7 +5,7 @@ from .block import Block from .auxiliary_blocks.jacobiandict_block import JacobianDictBlock from .support.parent import Parent -from ..classes import JacobianDict +from ..classes import ImpulseDict, JacobianDict from ..utilities.graph import block_sort, find_intermediate_inputs @@ -69,18 +69,18 @@ def _steady_state(self, calibration, dissolve, **kwargs): return ss - def _impulse_nonlinear(self, ss, inputs, outputs, Js, options): + def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() - irf_nonlin_partial_eq = inputs.copy() + impulses = inputs.copy() for block in self.blocks: - input_args = {k: v for k, v in irf_nonlin_partial_eq.items() if k in block.inputs} + input_args = {k: v for k, v in impulses.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_nonlin_partial_eq.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, Js, options)) + impulses.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, internals, Js, options)) - return irf_nonlin_partial_eq[original_outputs] + return ImpulseDict({k: impulses.toplevel[k] for k in original_outputs}, impulses.internals, impulses.T) def _impulse_linear(self, ss, inputs, outputs, Js, options): original_outputs = outputs diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 52bc82a..12e265c 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -106,29 +106,27 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internals}, {self.name: {k: ss[k] for k in ss if k in self.internals}}) - def _impulse_nonlinear(self, ssin, inputs, outputs, monotonic=False, returnindividual=False): + def _impulse_nonlinear(self, ssin, inputs, outputs, internals, monotonic=False): ss = self.extract_ss_dict(ssin) # identify individual variable paths we want from backward iteration, then run it toreturn = self.non_backward_outputs if self.hetoutputs is not None: toreturn = toreturn | self.hetoutputs.outputs + toreturn = (toreturn | internals) - ['D', 'Dbeg'] individual_paths, exog_path = self.backward_nonlinear(ss, inputs, toreturn) - # run forward iteration to get path of distribution (both Dbeg - what to do with this? - and D) - Dbeg_path, D_path = self.forward_nonlinear(ss, individual_paths, exog_path) + # run forward iteration to get path of distribution, add to individual_paths + self.forward_nonlinear(ss, individual_paths, exog_path) # obtain aggregates of all outputs, made uppercase - aggregates = {o.upper(): utils.optimized_routines.fast_aggregate(D_path, individual_paths[o]) - for o in individual_paths} + aggregates = {o: utils.optimized_routines.fast_aggregate( + individual_paths['D'], individual_paths[self.M_outputs.inv @ o]) for o in outputs} - # return either this, or also include distributional information - # TODO: rethink this when dealing with internals, including Dbeg - if returnindividual: - return ImpulseDict({**aggregates, **individual_paths, 'D': D_path}) - ssin - else: - return ImpulseDict(aggregates)[outputs] - ssin + # obtain internals + internals_dict = {self.name: {k: individual_paths[k] for k in internals}} + return ImpulseDict(aggregates, internals_dict, inputs.T) - ssin def _impulse_linear(self, ss, inputs, outputs, Js): return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) @@ -243,7 +241,7 @@ def forward_steady_state(self, ss, tol=1E-10, maxit=100_000): def backward_nonlinear(self, ss, inputs, toreturn): T = inputs.T - individual_paths = {k: np.empty((T,) + ss['D'].shape) for k in toreturn} + individual_paths = {k: np.empty((T,) + ss[k].shape) for k in toreturn} backdict = ss.copy() exog = self.make_exog_law_of_motion(backdict) @@ -286,7 +284,8 @@ def forward_nonlinear(self, ss, individual_paths, exog_path): Dbeg = endog.forward(D_path[t, ...]) Dbeg_path[t+1, ...] = Dbeg # make this optional - return Dbeg_path, D_path + individual_paths['D'] = D_path + individual_paths['Dbeg'] = Dbeg_path '''Jacobian calculation: four parts of fake news algorithm, plus support methods''' diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index c47e108..6f8cd9b 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -69,9 +69,9 @@ def _steady_state(self, calibration, dissolve, options, **kwargs): return self.block.solve_steady_state(calibration, unknowns, self.targets, options, **kwargs) - def _impulse_nonlinear(self, ss, inputs, outputs, Js, options, **kwargs): + def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, **kwargs): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs - self.unknowns.keys(), Js, options, **kwargs) + inputs, outputs - self.unknowns.keys(), internals, Js, options, **kwargs) def _impulse_linear(self, ss, inputs, outputs, Js, options): return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index 2a82ab4..d656373 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -22,46 +22,62 @@ def __init__(self, data, internals=None, T=None): self.T = (T if T is not None else self.infer_length()) def __add__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v + other for k, v in self.toplevel.items()}) - elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v + other[k] for k, v in self.toplevel.items()}) - else: - return NotImplementedError('Only a number or a SteadyStateDict can be added from an ImpulseDict.') + return self.binary_operation(other, lambda a, b: a + b) + + def __radd__(self, other): + return self.__add__(other) def __sub__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v - other for k, v in self.toplevel.items()}) - elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v - other[k] for k, v in self.toplevel.items()}) - else: - return NotImplementedError('Only a number or a SteadyStateDict can be subtracted from an ImpulseDict.') + return self.binary_operation(other, lambda a, b: a - b) + + def __rsub__(self, other): + return self.binary_operation(other, lambda a, b: b - a) def __mul__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.toplevel.items()}) - elif isinstance(other, (SteadyStateDict, ImpulseDict)): - return type(self)({k: v * other[k] for k, v in self.toplevel.items()}) - else: - return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') + return self.binary_operation(other, lambda a, b: a * b) def __rmul__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v * other for k, v in self.toplevel.items()}) - elif isinstance(other, SteadyStateDict): - return type(self)({k: v * other[k] for k, v in self.toplevel.items()}) - else: - return NotImplementedError('An ImpulseDict can only be multiplied by a number or a SteadyStateDict.') + return self.__mul__(other) def __truediv__(self, other): - if isinstance(other, (float, int)): - return type(self)({k: v / other for k, v in self.toplevel.items()}) - # ImpulseDict[['C, 'Y']] / ss[['C', 'Y']]: matches steady states; don't divide by zero - elif isinstance(other, SteadyStateDict): - return type(self)({k: v / other[k] if not np.isclose(other[k], 0) else v for k, v in self.toplevel.items()}) + return self.binary_operation(other, lambda a, b: a / b) + + def __rtruediv__(self, other): + return self.binary_operation(other, lambda a, b: b / a) + + def __neg__(self): + return self.unary_operation(lambda a: -a) + + def __pos__(self): + return self + + def __abs__(self): + return self.unary_operation(lambda a: abs(a)) + + def binary_operation(self, other, op): + if isinstance(other, (SteadyStateDict, ImpulseDict)): + toplevel = {k: op(v, other[k]) for k, v in self.toplevel.items()} + internals = {} + for b in self.internals: + other_internals = other.internals[b] + internals[b] = {k: op(v, other_internals[k]) for k, v in self.internals[b].items()} + return ImpulseDict(toplevel, internals, self.T) + elif isinstance(other, (float, int)): + toplevel = {k: op(v, other) for k, v in self.toplevel.items()} + internals = {} + for b in self.internals: + internals[b] = {k: op(v, other) for k, v in self.internals[b].items()} + return ImpulseDict(toplevel, internals, self.T) else: - return NotImplementedError('An ImpulseDict can only be divided by a number or a SteadyStateDict.') + return NotImplementedError(f'Can only perform operations with ImpulseDicts and other ImpulseDicts, SteadyStateDicts, or numbers, not {type(other).__name__}') + def unary_operation(self, op): + toplevel = {k: op(v) for k, v in self.toplevel.items()} + internals = {} + for b in self.internals: + internals[b] = {k: op(v) for k, v in self.internals[b].items()} + return ImpulseDict(toplevel, internals, self.T) + def pack(self): T = self.T bigv = np.empty(T*len(self.toplevel)) diff --git a/src/sequence_jacobian/utilities/bijection.py b/src/sequence_jacobian/utilities/bijection.py index 31f0098..cb889bd 100644 --- a/src/sequence_jacobian/utilities/bijection.py +++ b/src/sequence_jacobian/utilities/bijection.py @@ -25,7 +25,9 @@ def __getitem__(self, k): return self.map.get(k, k) def __matmul__(self, x): - if isinstance(x, Bijection): + if isinstance(x, str): + return self[x] + elif isinstance(x, Bijection): # compose self: v -> u with x: w -> v # assume everything missing in either is the identity M = {} @@ -50,7 +52,9 @@ def __matmul__(self, x): return NotImplemented def __rmatmul__(self, x): - if isinstance(x, dict): + if isinstance(x, str): + return self[x] + elif isinstance(x, dict): return {self[k]: v for k, v in x.items()} elif isinstance(x, list): return [self[k] for k in x] diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index c7ee0d8..57a3c61 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -147,7 +147,9 @@ def test_all(): # Nonlinear vs linear impulses td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) + inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js, internals=['household']) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') + # See if D change matches up with aggregate assets + assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['a'], axis=(1,2)), td_nonlin['A']) From 13fa636494b551b458d0151cd37055ea8865077f Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 15 Sep 2021 14:19:07 -0400 Subject: [PATCH 271/288] =?UTF-8?q?Refactored=20=1Bxamples/=20so=20that=20?= =?UTF-8?q?generic=20hetblocks=20are=20embedded=20in=20specific=20models.?= =?UTF-8?q?=20Caught=20some=20bugs:=20hetinputs=20and=20hetoutputs=20now?= =?UTF-8?q?=20work=20from=20the=20decorator.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sequence_jacobian/blocks/het_block.py | 10 +- src/sequence_jacobian/examples/__init__.py | 1 + src/sequence_jacobian/examples/hank.py | 106 +++++ .../examples/hetblocks/__init__.py | 1 + .../examples/hetblocks/household_labor.py | 90 ++++ .../examples/hetblocks/household_sim.py | 36 ++ .../examples/hetblocks/household_twoasset.py | 184 ++++++++ .../examples/krusell_smith.py | 90 ++++ src/sequence_jacobian/examples/rbc.py | 48 ++ src/sequence_jacobian/examples/two_asset.py | 191 ++++++++ src/sequence_jacobian/models/__init__.py | 1 - src/sequence_jacobian/models/hank.py | 224 --------- src/sequence_jacobian/models/krusell_smith.py | 137 ------ src/sequence_jacobian/models/rbc.py | 94 ---- src/sequence_jacobian/models/two_asset.py | 424 ------------------ src/sequence_jacobian/utilities/function.py | 5 +- tests/base/test_estimation.py | 5 +- tests/base/test_jacobian.py | 6 +- tests/base/test_jacobian_dict_block.py | 6 +- tests/base/test_options.py | 34 +- tests/base/test_public_classes.py | 2 +- tests/base/test_steady_state.py | 58 +-- tests/base/test_transitional_dynamics.py | 25 +- tests/base/test_two_asset.py | 21 +- tests/conftest.py | 109 +---- tests/robustness/test_steady_state.py | 10 +- 26 files changed, 840 insertions(+), 1078 deletions(-) create mode 100644 src/sequence_jacobian/examples/__init__.py create mode 100644 src/sequence_jacobian/examples/hank.py create mode 100644 src/sequence_jacobian/examples/hetblocks/__init__.py create mode 100644 src/sequence_jacobian/examples/hetblocks/household_labor.py create mode 100644 src/sequence_jacobian/examples/hetblocks/household_sim.py create mode 100644 src/sequence_jacobian/examples/hetblocks/household_twoasset.py create mode 100644 src/sequence_jacobian/examples/krusell_smith.py create mode 100644 src/sequence_jacobian/examples/rbc.py create mode 100644 src/sequence_jacobian/examples/two_asset.py delete mode 100644 src/sequence_jacobian/models/__init__.py delete mode 100644 src/sequence_jacobian/models/hank.py delete mode 100644 src/sequence_jacobian/models/krusell_smith.py delete mode 100644 src/sequence_jacobian/models/rbc.py delete mode 100644 src/sequence_jacobian/models/two_asset.py diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 12e265c..0c24b1e 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -40,11 +40,11 @@ def __init__(self, backward_fun, exogenous, policy, backward, backward_init=None self.original_M_outputs = self.M_outputs # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. - # See docstring for methods `add_hetinput` and `add_hetoutput` for more details. - self.hetinputs = hetinputs - self.hetoutputs = hetoutputs - if hetinputs is not None or hetoutputs is not None: - self.process_hetinputs_hetoutputs(hetinputs, hetoutputs, tocopy=False) + if hetinputs is not None: + hetinputs = ExtendedParallelFunction(hetinputs) + if hetoutputs is not None: + hetoutputs = ExtendedParallelFunction(hetoutputs) + self.process_hetinputs_hetoutputs(hetinputs, hetoutputs, tocopy=False) if len(self.policy) > 2: raise ValueError(f"More than two endogenous policies in {self.name}, not yet supported") diff --git a/src/sequence_jacobian/examples/__init__.py b/src/sequence_jacobian/examples/__init__.py new file mode 100644 index 0000000..50e3a0c --- /dev/null +++ b/src/sequence_jacobian/examples/__init__.py @@ -0,0 +1 @@ +"""Example models""" \ No newline at end of file diff --git a/src/sequence_jacobian/examples/hank.py b/src/sequence_jacobian/examples/hank.py new file mode 100644 index 0000000..946707b --- /dev/null +++ b/src/sequence_jacobian/examples/hank.py @@ -0,0 +1,106 @@ +import numpy as np + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.combined_block import create_model, combine +from .hetblocks import household_labor as hh + + +'''Part 1: Blocks''' + + +@simple +def firm(Y, w, Z, pi, mu, kappa): + L = Y / Z + Div = Y - w * L - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y + return L, Div + + +@simple +def monetary(pi, rstar, phi): + r = (1 + rstar(-1) + phi * pi(-1)) / (1 + pi) - 1 + return r + + +@simple +def fiscal(r, B): + Tax = r * B + return Tax + + +@simple +def mkt_clearing(A, N_E, C, L, Y, B, pi, mu, kappa): + asset_mkt = A - B + labor_mkt = N_E - L + goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y + return asset_mkt, labor_mkt, goods_mkt + + +@simple +def nkpc(pi, w, Z, Y, r, mu, kappa): + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ + - (1 + pi).apply(np.log) + return nkpc_res + + +#TODO: this works but seems wrong conceptually +@simple +def partial_ss_solution(B_Y, Y, mu, r, kappa, Z, pi): + B = B_Y + w = 1 / mu + Div = (1 - w) + Tax = r * B + + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) + + return B, w, Div, Tax, nkpc_res + + +'''Part 2: Embed HA block''' + + +# This cannot be a hetinput, bc `transfers` depends on it +@simple +def make_grids(rho_s, sigma_s, nS, amax, nA): + e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return e_grid, pi_e, Pi, a_grid + + +def transfers(pi_e, Div, Tax, e_grid): + # default incidence rules are proportional to skill; scale does not matter + tax_rule, div_rule = e_grid, e_grid + + div = Div / np.sum(pi_e * div_rule) * div_rule + tax = Tax / np.sum(pi_e * tax_rule) * tax_rule + T = div - tax + return T + + +'''Part 3: DAG''' + +def dag(): + # Combine blocks + household = hh.household.add_hetinputs([transfers]) + household = combine([make_grids, household], name='HH') + blocks = [household, firm, monetary, fiscal, mkt_clearing, nkpc] + helper_blocks = [partial_ss_solution] + hank_model = create_model(blocks, name="One-Asset HANK") + + # Steady state + calibration = {'r': 0.005, 'rstar': 0.005, 'eis': 0.5, 'frisch': 0.5, 'B_Y': 5.6, + 'mu': 1.2, 'rho_s': 0.966, 'sigma_s': 0.5, 'kappa': 0.1, 'phi': 1.5, + 'Y': 1, 'Z': 1, 'L': 1, 'pi': 0, 'nS': 2, 'amax': 150, 'nA': 10} + unknowns_ss = {'beta': 0.986, 'vphi': 0.8, 'w': 0.8} + targets_ss = {'asset_mkt': 0, 'labor_mkt': 0, 'nkpc_res': 0.} + ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + solver='broyden_custom', + helper_blocks=helper_blocks, + helper_targets=['nkpc_res']) + + # Transitional dynamics + unknowns = ['pi', 'w', 'Y'] + targets = ['nkpc_res', 'asset_mkt', 'labor_mkt'] + exogenous = ['rstar', 'Z'] + + return hank_model, ss, unknowns, targets, exogenous diff --git a/src/sequence_jacobian/examples/hetblocks/__init__.py b/src/sequence_jacobian/examples/hetblocks/__init__.py new file mode 100644 index 0000000..0fcb820 --- /dev/null +++ b/src/sequence_jacobian/examples/hetblocks/__init__.py @@ -0,0 +1 @@ +'''Heterogeneous agent blocks''' \ No newline at end of file diff --git a/src/sequence_jacobian/examples/hetblocks/household_labor.py b/src/sequence_jacobian/examples/hetblocks/household_labor.py new file mode 100644 index 0000000..ad17dba --- /dev/null +++ b/src/sequence_jacobian/examples/hetblocks/household_labor.py @@ -0,0 +1,90 @@ +'''Standard Incomplete Market model with Endogenous Labor Supply''' + +import numpy as np +from numba import vectorize, njit + +from ...blocks.het_block import het +from ... import utilities as utils + + +def household_init(a_grid, e_grid, r, w, eis, T): + fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + return fininc, Va + + +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): + '''Single backward step via EGM.''' + # TODO: ws shoukld be hetinput + ws = w * e_grid + uc_nextgrid = beta * Va_p + c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) + + lhs = c_nextgrid - ws[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] + rhs = (1 + r) * a_grid + c = utils.interpolate.interpolate_y(lhs, rhs, c_nextgrid) + n = utils.interpolate.interpolate_y(lhs, rhs, n_nextgrid) + + a = rhs + ws[:, np.newaxis] * n + T[:, np.newaxis] - c + iconst = np.nonzero(a < a_grid[0]) + a[iconst] = a_grid[0] + + if iconst[0].size != 0 and iconst[1].size != 0: + c[iconst], n[iconst] = solve_cn(ws[iconst[0]], + rhs[iconst[1]] + T[iconst[0]] - a_grid[0], + eis, frisch, vphi, Va_p[iconst]) + + Va = (1 + r) * c ** (-1 / eis) + + # TODO: make hetoutput + n_e = e_grid[:, np.newaxis] * n + + return Va, a, c, n, n_e + + +'''Supporting functions for HA block''' + +@njit +def cn(uc, w, eis, frisch, vphi): + """Return optimal c, n as function of u'(c) given parameters""" + return uc ** (-eis), (w * uc / vphi) ** frisch + + +def solve_cn(w, T, eis, frisch, vphi, uc_seed): + uc = solve_uc(w, T, eis, frisch, vphi, uc_seed) + return cn(uc, w, eis, frisch, vphi) + + +@vectorize +def solve_uc(w, T, eis, frisch, vphi, uc_seed): + """Solve for optimal uc given in log uc space. + + max_{c, n} c**(1-1/eis) + vphi*n**(1+1/frisch) s.t. c = w*n + T + """ + log_uc = np.log(uc_seed) + for i in range(30): + ne, ne_p = netexp(log_uc, w, T, eis, frisch, vphi) + if abs(ne) < 1E-11: + break + else: + log_uc -= ne / ne_p + else: + raise ValueError("Cannot solve constrained household's problem: No convergence after 30 iterations!") + + return np.exp(log_uc) + + +@njit +def netexp(log_uc, w, T, eis, frisch, vphi): + """Return net expenditure as a function of log uc and its derivative.""" + c, n = cn(np.exp(log_uc), w, eis, frisch, vphi) + ne = c - w * n - T + + # c and n have elasticities of -eis and frisch wrt log u'(c) + c_loguc = -eis * c + n_loguc = frisch * n + netexp_loguc = c_loguc - w * n_loguc + + return ne, netexp_loguc diff --git a/src/sequence_jacobian/examples/hetblocks/household_sim.py b/src/sequence_jacobian/examples/hetblocks/household_sim.py new file mode 100644 index 0000000..2ed4547 --- /dev/null +++ b/src/sequence_jacobian/examples/hetblocks/household_sim.py @@ -0,0 +1,36 @@ +'''Standard Incomplete Market model''' + +import numpy as np + +from ...blocks.het_block import het +from ... import utilities as utils + + +def household_init(a_grid, e_grid, r, w, eis): + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + Va = (1 + r) * (0.1 * coh) ** (-1 / eis) + return Va + + +def income_state_vars(rho, sigma, nS): + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + return e_grid, Pi + + +def asset_state_vars(amax, nA): + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return a_grid + + +@het(exogenous='Pi', policy='a', backward='Va', + hetinputs=[income_state_vars, asset_state_vars], backward_init=household_init) +def household(Va_p, a_grid, e_grid, r, w, beta, eis): + uc_nextgrid = beta * Va_p + c_nextgrid = uc_nextgrid ** (-eis) + coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) + utils.optimized_routines.setmin(a, a_grid[0]) + c = coh - a + Va = (1 + r) * c ** (-1 / eis) + return Va, a, c + \ No newline at end of file diff --git a/src/sequence_jacobian/examples/hetblocks/household_twoasset.py b/src/sequence_jacobian/examples/hetblocks/household_twoasset.py new file mode 100644 index 0000000..6f18f1d --- /dev/null +++ b/src/sequence_jacobian/examples/hetblocks/household_twoasset.py @@ -0,0 +1,184 @@ +import numpy as np +from numba import guvectorize + +from ...blocks.het_block import het +from ...blocks.support.simple_displacement import apply_function +from ... import utilities as utils + + +def household_init(b_grid, a_grid, z_grid, eis): + Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) + return Va, Vb + + +def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): + chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) + return chi + + +# policy and bacward order as in grid! +@het(exogenous='Pi', policy=['b', 'a'], backward=['Vb', 'Va'], + hetoutputs=[adjustment_costs], backward_init=household_init) +def household(Va_p, Vb_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): + # TODO: make into hetinput + # precompute Psi1(a', a) on grid of (a', a) for steps 3 and 5 + Psi1 = get_Psi_and_deriv(a_grid[:, np.newaxis], + a_grid[np.newaxis, :], ra, chi0, chi1, chi2)[1] + + # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === + # (take discounted expectation of tomorrow's value function) + Wb = beta * Vb_p + Wa = beta * Va_p + W_ratio = Wa / Wb + + # === STEP 3: a'(z, b', a) for UNCONSTRAINED === + + # for each (z, b', a), linearly interpolate to find a' between gridpoints + # satisfying optimality condition W_ratio == 1+Psi1 + i, pi = lhs_equals_rhs_interpolate(W_ratio, 1 + Psi1) + + # use same interpolation to get Wb and then c + a_endo_unc = utils.interpolate.apply_coord(i, pi, a_grid) + c_endo_unc = utils.interpolate.apply_coord(i, pi, Wb) ** (-eis) + + # === STEP 4: b'(z, b, a), a'(z, b, a) for UNCONSTRAINED === + + # solve out budget constraint to get b(z, b', a) + b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + + # interpolate this b' -> b mapping to get b -> b', so we have b'(z, b, a) + # and also use interpolation to get a'(z, b, a) + # (note utils.interpolate.interpolate_coord and utils.interpolate.apply_coord work on last axis, + # so we need to swap 'b' to the last axis, then back when done) + i, pi = utils.interpolate.interpolate_coord(b_endo.swapaxes(1, 2), b_grid) + a_unc = utils.interpolate.apply_coord(i, pi, a_endo_unc.swapaxes(1, 2)).swapaxes(1, 2) + b_unc = utils.interpolate.apply_coord(i, pi, b_grid).swapaxes(1, 2) + + # === STEP 5: a'(z, kappa, a) for CONSTRAINED === + + # for each (z, kappa, a), linearly interpolate to find a' between gridpoints + # satisfying optimality condition W_ratio/(1+kappa) == 1+Psi1, assuming b'=0 + lhs_con = W_ratio[:, 0:1, :] / (1 + k_grid[np.newaxis, :, np.newaxis]) + i, pi = lhs_equals_rhs_interpolate(lhs_con, 1 + Psi1) + + # use same interpolation to get Wb and then c + a_endo_con = utils.interpolate.apply_coord(i, pi, a_grid) + c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis]) ** (-eis) + * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) + + # === STEP 6: a'(z, b, a) for CONSTRAINED === + + # solve out budget constraint to get b(z, kappa, a), enforcing b'=0 + b_endo = (c_endo_con + a_endo_con + + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1 + ra) * a_grid) + + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) + + # interpolate this kappa -> b mapping to get b -> kappa + # then use the interpolated kappa to get a', so we have a'(z, b, a) + # (utils.interpolate.interpolate_y does this in one swoop, but since it works on last + # axis, we need to swap kappa to last axis, and then b back to middle when done) + a_con = utils.interpolate.interpolate_y(b_endo.swapaxes(1, 2), b_grid, + a_endo_con.swapaxes(1, 2)).swapaxes(1, 2) + + # === STEP 7: obtain policy functions and update derivatives of value function === + + # combine unconstrained solution and constrained solution, choosing latter + # when unconstrained goes below minimum b + a, b = a_unc.copy(), b_unc.copy() + b[b <= b_grid[0]] = b_grid[0] + a[b <= b_grid[0]] = a_con[b <= b_grid[0]] + + # calculate adjustment cost and its derivative + Psi, _, Psi2 = get_Psi_and_deriv(a, a_grid, ra, chi0, chi1, chi2) + + # solve out budget constraint to get consumption and marginal utility + c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b + uc = c ** (-1 / eis) + + # TODO: maken into hetoutpout + # for GE wage Phillips curve we'll need endowment-weighted utility too + u = e_grid[:, np.newaxis, np.newaxis] * uc + + # update derivatives of value function using envelope conditions + Va = (1 + ra - Psi2) * uc + Vb = (1 + rb) * uc + + return Va, Vb, a, b, c, u + + +'''Supporting functions for HA block''' + +def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): + """Adjustment cost Psi(ap, a) and its derivatives with respect to + first argument (ap) and second argument (a)""" + a_with_return = (1 + ra) * a + a_change = ap - a_with_return + abs_a_change = np.abs(a_change) + sign_change = np.sign(a_change) + + adj_denominator = a_with_return + chi0 + core_factor = (abs_a_change / adj_denominator) ** (chi2 - 1) + + Psi = chi1 / chi2 * abs_a_change * core_factor + Psi1 = chi1 * sign_change * core_factor + Psi2 = -(1 + ra) * (Psi1 + (chi2 - 1) * Psi / adj_denominator) + return Psi, Psi1, Psi2 + + +def matrix_times_first_dim(A, X): + """Take matrix A times vector X[:, i1, i2, i3, ... , in] separately + for each i1, i2, i3, ..., in. Same output as A @ X if X is 1D or 2D""" + # flatten all dimensions of X except first, then multiply, then restore shape + return (A @ X.reshape(X.shape[0], -1)).reshape(X.shape) + + +def addouter(z, b, a): + """Take outer sum of three arguments: result[i, j, k] = z[i] + b[j] + a[k]""" + return z[:, np.newaxis, np.newaxis] + b[:, np.newaxis] + a + + +@guvectorize(['void(float64[:], float64[:,:], uint32[:], float64[:])'], '(ni),(ni,nj)->(nj),(nj)') +def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): + """ + Given lhs (i) and rhs (i,j), for each j, find the i such that + + lhs[i] > rhs[i,j] and lhs[i+1] < rhs[i+1,j] + + i.e. where given j, lhs == rhs in between i and i+1. + + Also return the pi such that + + pi*(lhs[i] - rhs[i,j]) + (1-pi)*(lhs[i+1] - rhs[i+1,j]) == 0 + + i.e. such that the point at pi*i + (1-pi)*(i+1) satisfies lhs == rhs by linear interpolation. + + If lhs[0] < rhs[0,j] already, just return u=0 and pi=1. + + ***IMPORTANT: Assumes that solution i is monotonically increasing in j + and that lhs - rhs is monotonically decreasing in i.*** + """ + + ni, nj = rhs.shape + assert len(lhs) == ni + + i = 0 + for j in range(nj): + while True: + if lhs[i] < rhs[i, j]: + break + elif i < nj - 1: + i += 1 + else: + break + + if i == 0: + iout[j] = 0 + piout[j] = 1 + else: + iout[j] = i - 1 + err_upper = rhs[i, j] - lhs[i] + err_lower = rhs[i - 1, j] - lhs[i - 1] + piout[j] = err_upper / (err_upper - err_lower) + \ No newline at end of file diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py new file mode 100644 index 0000000..66bcacb --- /dev/null +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -0,0 +1,90 @@ +import scipy.optimize as opt + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.combined_block import create_model +from .hetblocks.household_sim import household + + +'''Part 1: Blocks''' + +@simple +def firm(K, L, Z, alpha, delta): + r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta + w = (1 - alpha) * Z * (K(-1) / L) ** alpha + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return r, w, Y + + +@simple +def mkt_clearing(K, A, Y, C, delta): + asset_mkt = A - K + goods_mkt = Y - C - delta * K + return asset_mkt, goods_mkt + + +@simple +def firm_ss_solution(r, Y, L, delta, alpha): + rk = r + delta + w = (1 - alpha) * Y / L + K = alpha * Y / rk + Z = Y / K ** alpha / L ** (1 - alpha) + return w, K, Z + + +'''Part 2: DAG''' + +def dag(): + # Combine blocks + blocks = [household, firm, mkt_clearing] + helper_blocks = [firm_ss_solution] + ks_model = create_model(blocks, name="Krusell-Smith") + + # Steady state + calibration = {'eis': 1, 'delta': 0.025, 'alpha': 0.11, 'rho': 0.966, 'sigma': 0.5, + 'L': 1.0, 'nS': 2, 'nA': 10, 'amax': 200, 'r': 0.01} + unknowns_ss = {'beta': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.85, 'K': 3.} + targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} + ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', + helper_blocks=helper_blocks, helper_targets=['Y', 'r']) + + # Transitional dynamics + inputs = ['Z'] + unknowns = ['K'] + targets = ['asset_mkt'] + + return ks_model, ss, unknowns, targets, inputs + + +'''Part 3: Permanent beta heterogeneity''' + +@simple +def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): + C = mass_patient * C_patient + (1 - mass_patient) * C_impatient + A = mass_patient * A_patient + (1 - mass_patient) * A_impatient + return C, A + + +def remapped_dag(): + # Create 2 versions of the household block using `remap` + to_map = ['beta', *household.outputs] + hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') + hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') + blocks = [hh_patient, hh_impatient, firm, mkt_clearing, aggregate] + ks_remapped = create_model(blocks, name='KS-beta-het') + + # Steady State + calibration = {'eis': 1., 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, + 'nS': 3, 'nA': 100, 'amax': 1000, 'beta_impatient': 0.985, 'mass_patient': 0.5} + unknowns_ss = {'beta_patient': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.5, 'K': 8.} + targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} + helper_blocks = [firm_ss_solution] + ss = ks_remapped.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', + helper_blocks=helper_blocks, helper_targets=['Y', 'r']) + + # Transitional Dynamics/Jacobian Calculation + unknowns = ['K'] + targets = ['asset_mkt'] + exogenous = ['Z'] + + return ks_remapped, ss, unknowns, targets, ss, exogenous diff --git a/src/sequence_jacobian/examples/rbc.py b/src/sequence_jacobian/examples/rbc.py new file mode 100644 index 0000000..934bd5f --- /dev/null +++ b/src/sequence_jacobian/examples/rbc.py @@ -0,0 +1,48 @@ +from ..blocks.simple_block import simple +from ..blocks.combined_block import create_model + + +'''Part 1: Blocks''' + +@simple +def firm(K, L, Z, alpha, delta): + r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta + w = (1 - alpha) * Z * (K(-1) / L) ** alpha + Y = Z * K(-1) ** alpha * L ** (1 - alpha) + return r, w, Y + + +@simple +def household(K, L, w, eis, frisch, vphi, delta): + C = (w / vphi / L ** (1 / frisch)) ** eis + I = K - (1 - delta) * K(-1) + return C, I + + +@simple +def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): + goods_mkt = Y - C - I + euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) + walras = C + K - (1 + r) * K(-1) - w * L + return goods_mkt, euler, walras + + +'''Part 2: Assembling the model''' + +def dag(): + # Combine blocks + blocks = [household, firm, mkt_clearing] + rbc_model = create_model(blocks, name="RBC") + + # Steady state + calibration = {'eis': 1., 'frisch': 1., 'delta': 0.025, 'alpha': 0.11, 'L': 1.} + unknowns_ss = {'vphi': 0.92, 'beta': 1 / (1 + 0.01), 'K': 2., 'Z': 1.} + targets_ss = {'goods_mkt': 0., 'r': 0.01, 'euler': 0., 'Y': 1.} + ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='hybr') + + # Transitional dynamics + unknowns = ['K', 'L'] + targets = ['goods_mkt', 'euler'] + exogenous = ['Z'] + + return rbc_model, ss, unknowns, targets, exogenous diff --git a/src/sequence_jacobian/examples/two_asset.py b/src/sequence_jacobian/examples/two_asset.py new file mode 100644 index 0000000..e046b31 --- /dev/null +++ b/src/sequence_jacobian/examples/two_asset.py @@ -0,0 +1,191 @@ +import numpy as np + +from .. import utilities as utils +from ..blocks.simple_block import simple +from ..blocks.solved_block import solved +from ..blocks.combined_block import create_model, combine +from .hetblocks import household_twoasset as hh + + +'''Part 1: Blocks''' + +@simple +def pricing(pi, mc, r, Y, kappap, mup): + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) \ + / (1 + r(+1)) - (1 + pi).apply(np.log) + return nkpc + + +@simple +def arbitrage(div, p, r): + equity = div(+1) + p(+1) - p * (1 + r(+1)) + return equity + + +@simple +def labor(Y, w, K, Z, alpha): + N = (Y / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) + mc = w * N / (1 - alpha) / Y + return N, mc + + +@simple +def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): + inv = (K / K(-1) - 1) / (delta * epsI) + 1 - Q + val = alpha * Z(+1) * (N(+1) / K) ** (1 - alpha) * mc(+1) -\ + (K(+1) / K - (1 - delta) + (K(+1) / K - 1) ** 2 / (2 * delta * epsI)) +\ + K(+1) / K * Q(+1) - (1 + r(+1)) * Q + return inv, val + + +@simple +def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): + psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y + k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) + I = K - (1 - delta) * K(-1) + k_adjust + div = Y - w * N - I - psip + return psip, I, div + + +@simple +def taylor(rstar, pi, phi): + i = rstar + phi * pi + return i + + +@simple +def fiscal(r, w, N, G, Bg): + tax = (r * Bg + G) / w / N + return tax + + +@simple +def finance(i, p, pi, r, div, omega, pshare): + rb = r - omega + ra = pshare(-1) * (div + p) / p(-1) + (1 - pshare(-1)) * (1 + r) - 1 + fisher = 1 + i(-1) - (1 + r) * (1 + pi) + return rb, ra, fisher + + +@simple +def wage(pi, w): + piw = (1 + pi) * w / w(-1) - 1 + return piw + + +@simple +def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): + wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ + (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) + return wnkpc + + +@simple +def mkt_clearing(p, A, B, Bg, C, I, G, CHI, psip, omega, Y): + wealth = A + B + asset_mkt = p + Bg - wealth + goods_mkt = C + I + G + CHI + psip + omega * B - Y + return asset_mkt, wealth, goods_mkt + + +@simple +def share_value(p, tot_wealth, Bh): + pshare = p / (tot_wealth - Bh) + return pshare + + +@solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver="brentq") +def pricing_solved(pi, mc, r, Y, kappap, mup): + nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ + (1 + r(+1)) - (1 + pi).apply(np.log) + return nkpc + + +@solved(unknowns={'p': (5, 15)}, targets=['equity'], solver="brentq") +def arbitrage_solved(div, p, r): + equity = div(+1) + p(+1) - p * (1 + r(+1)) + return equity + + +@simple +def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): + """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" + # 1. Solve for markup to hit total wealth + p = tot_wealth - Bg + mc = 1 - r * (p - K) / Y + mup = 1 / mc + wealth = tot_wealth + + # 2. Solve for capital share to hit K + alpha = (r + delta) * K / Y / mc + + # 3. Solve for TFP to hit Y + Z = Y * K ** (-alpha) * N ** (alpha - 1) + + # 4. Solve for w such that piw = 0 + w = mc * (1 - alpha) * Y / N + piw = 0 + + return p, mc, mup, wealth, alpha, Z, w, piw + + +@simple +def partial_ss_step2(tax, w, U, N, muw, frisch): + """Solves for (vphi) to hit (wnkpc).""" + vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) + wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw + return vphi, wnkpc + + +'''Part 2: Embed HA block''' + +# This cannot be a hetinput, bc `income` depends on it +@simple +def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): + b_grid = utils.discretize.agrid(amax=bmax, n=nB) + a_grid = utils.discretize.agrid(amax=amax, n=nA) + k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) + return b_grid, a_grid, k_grid, e_grid, Pi + + +def income(e_grid, tax, w, N): + z_grid = (1 - tax) * w * N * e_grid + return z_grid + + +'''Part 3: DAG''' + +def dag(): + # Combine Blocks + household = hh.household.add_hetinputs([income]) + household = combine([make_grids, household], name='HH') + production = combine([labor, investment]) + production_solved = production.solved(unknowns={'Q': 1., 'K': 10.}, + targets=['inv', 'val'], solver='broyden_custom') + blocks = [household, pricing_solved, arbitrage_solved, production_solved, + dividend, taylor, fiscal, share_value, finance, wage, union, mkt_clearing] + helper_blocks = [partial_ss_step1, partial_ss_step2] + two_asset_model = create_model(blocks, name='Two-Asset HANK') + + # Steady State + calibration = {'Y': 1., 'r': 0.0125, 'rstar': 0.0125, 'tot_wealth': 14, 'delta': 0.02, + 'kappap': 0.1, 'muw': 1.1, 'Bh': 1.04, 'Bg': 2.8, 'G': 0.2, 'eis': 0.5, + 'frisch': 1, 'chi0': 0.25, 'chi2': 2, 'epsI': 4, 'omega': 0.005, + 'kappaw': 0.1, 'phi': 1.5, 'nZ': 3, 'nB': 10, 'nA': 16, 'nK': 4, + 'bmax': 50, 'amax': 4000, 'kmax': 1, 'rho_z': 0.966, 'sigma_z': 0.92} + unknowns_ss = {'beta': 0.976, 'chi1': 6.5, 'vphi': 1.71, 'Z': 0.4678, + 'alpha': 0.3299, 'mup': 1.015, 'w': 0.66} + targets_ss = {'asset_mkt': 0., 'B': 'Bh', 'wnkpc': 0., 'piw': 0.0, 'K': 10., + 'wealth': 'tot_wealth', 'N': 1.0} + ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, + solver='broyden_custom', + helper_blocks=helper_blocks, + helper_targets=['wnkpc', 'piw', 'K', 'wealth', 'N']) + + # Transitional Dynamics/Jacobian Calculation + unknowns = ['r', 'w', 'Y'] + targets = ['asset_mkt', 'fisher', 'wnkpc'] + exogenous = ['rstar', 'Z', 'G'] + + return two_asset_model, ss, unknowns, targets, exogenous diff --git a/src/sequence_jacobian/models/__init__.py b/src/sequence_jacobian/models/__init__.py deleted file mode 100644 index 29259f6..0000000 --- a/src/sequence_jacobian/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Specific Model Implementations""" \ No newline at end of file diff --git a/src/sequence_jacobian/models/hank.py b/src/sequence_jacobian/models/hank.py deleted file mode 100644 index c5ce817..0000000 --- a/src/sequence_jacobian/models/hank.py +++ /dev/null @@ -1,224 +0,0 @@ -import numpy as np -from numba import vectorize, njit - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het - - -'''Part 1: HA block''' - - -def household_init(a_grid, e_grid, r, w, eis, T): - fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - return fininc, Va - - -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): - """Single backward iteration step using endogenous gridpoint method for households with separable CRRA utility.""" - # this one is useful to do internally - ws = w * e_grid - - # uc(z_t, a_t) - uc_nextgrid = beta * Va_p - - # c(z_t, a_t) and n(z_t, a_t) - c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) - - # c(z_t, a_{t-1}) and n(z_t, a_{t-1}) - lhs = c_nextgrid - ws[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] - rhs = (1 + r) * a_grid - c = utils.interpolate.interpolate_y(lhs, rhs, c_nextgrid) - n = utils.interpolate.interpolate_y(lhs, rhs, n_nextgrid) - - # test constraints, replace if needed - a = rhs + ws[:, np.newaxis] * n + T[:, np.newaxis] - c - iconst = np.nonzero(a < a_grid[0]) - a[iconst] = a_grid[0] - - # if there exist states/prior asset levels such that households want to borrow, compute the constrained - # solution for consumption and labor supply - if iconst[0].size != 0 and iconst[1].size != 0: - c[iconst], n[iconst] = solve_cn(ws[iconst[0]], rhs[iconst[1]] + T[iconst[0]] - a_grid[0], - eis, frisch, vphi, Va_p[iconst]) - - # calculate marginal utility to go backward - Va = (1 + r) * c ** (-1 / eis) - - # efficiency units of labor which is what really matters - n_e = e_grid[:, np.newaxis] * n - - return Va, a, c, n, n_e - - -def transfers(pi_e, Div, Tax, e_grid): - # default incidence rules are proportional to skill - tax_rule, div_rule = e_grid, e_grid # scale does not matter, will be normalized anyway - - div = Div / np.sum(pi_e * div_rule) * div_rule - tax = Tax / np.sum(pi_e * tax_rule) * tax_rule - T = div - tax - return T - - -household = household.add_hetinputs([transfers]) - - -@njit -def cn(uc, w, eis, frisch, vphi): - """Return optimal c, n as function of u'(c) given parameters""" - return uc ** (-eis), (w * uc / vphi) ** frisch - - -def solve_cn(w, T, eis, frisch, vphi, uc_seed): - uc = solve_uc(w, T, eis, frisch, vphi, uc_seed) - return cn(uc, w, eis, frisch, vphi) - - -@vectorize -def solve_uc(w, T, eis, frisch, vphi, uc_seed): - """Solve for optimal uc given in log uc space. - - max_{c, n} c**(1-1/eis) + vphi*n**(1+1/frisch) s.t. c = w*n + T - """ - log_uc = np.log(uc_seed) - for i in range(30): - ne, ne_p = netexp(log_uc, w, T, eis, frisch, vphi) - if abs(ne) < 1E-11: - break - else: - log_uc -= ne / ne_p - else: - raise ValueError("Cannot solve constrained household's problem: No convergence after 30 iterations!") - - return np.exp(log_uc) - - -@njit -def netexp(log_uc, w, T, eis, frisch, vphi): - """Return net expenditure as a function of log uc and its derivative.""" - c, n = cn(np.exp(log_uc), w, eis, frisch, vphi) - ne = c - w * n - T - - # c and n have elasticities of -eis and frisch wrt log u'(c) - c_loguc = -eis * c - n_loguc = frisch * n - netexp_loguc = c_loguc - w * n_loguc - - return ne, netexp_loguc - - -'''Part 2: Simple blocks and hetinput''' - - -@simple -def firm(Y, w, Z, pi, mu, kappa): - L = Y / Z - Div = Y - w * L - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y - return L, Div - - -@simple -def monetary(pi, rstar, phi): - r = (1 + rstar(-1) + phi * pi(-1)) / (1 + pi) - 1 - return r - - -@simple -def fiscal(r, B): - Tax = r * B - return Tax - - -@simple -def mkt_clearing(A, N_E, C, L, Y, B, pi, mu, kappa): - asset_mkt = A - B - labor_mkt = N_E - L - goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y - return asset_mkt, labor_mkt, goods_mkt - - -@simple -def nkpc(pi, w, Z, Y, r, mu, kappa): - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ - - (1 + pi).apply(np.log) - return nkpc_res - - -@simple -def income_state_vars(rho_s, sigma_s, nS): - e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) - return e_grid, pi_e, Pi - - -@simple -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@simple -def partial_steady_state_solution(B_Y, Y, mu, r, kappa, Z, pi): - B = B_Y - w = 1 / mu - Div = (1 - w) - Tax = r * B - - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) - - return B, w, Div, Tax, nkpc_res - - -'''Part 3: Steady state''' - - -def hank_ss(beta_guess=0.986, vphi_guess=0.8, r=0.005, eis=0.5, frisch=0.5, mu=1.2, B_Y=5.6, rho_s=0.966, sigma_s=0.5, - kappa=0.1, phi=1.5, nS=7, amax=150, nA=500): - """Solve steady state of full GE model. Calibrate (beta, vphi) to hit target for interest rate and Y.""" - - # set up grid - a_grid = utils.discretize.agrid(amax=amax, n=nA) - e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) - - # solve analytically what we can - B = B_Y - w = 1 / mu - Div = (1 - w) - Tax = r * B - T = transfers(pi_e, Div, Tax, e_grid) - - calibration = {'Pi': Pi, 'a_grid': a_grid, 'e_grid': e_grid, 'pi_e': pi_e, 'w': w, 'r': r, 'eis': eis, 'Div': Div, - 'Tax': Tax, 'frisch': frisch} - - # residual function - def res(x): - beta_loc, vphi_loc = x - # precompute constrained c and n which don't depend on Va - if beta_loc > 0.999 / (1 + r) or vphi_loc < 0.001: - raise ValueError('Clearly invalid inputs') - - calibration['beta'], calibration['vphi'] = beta_loc, vphi_loc - out = household.steady_state(calibration) - - return np.array([out['A'] - B, out['N_E'] - 1]) - - # solve for beta, vphi - (beta, vphi), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, vphi_guess]), verbose=False) - calibration['beta'], calibration['vphi'] = beta, vphi - - # extra evaluation for reporting - ss = household.steady_state(calibration) - - # check Walras's law - goods_mkt = 1 - ss['C'] - assert np.abs(goods_mkt) < 1E-8 - - # add aggregate variables - ss.update({'Pi': Pi, 'B': B, 'phi': phi, 'kappa': kappa, 'Y': 1, 'rstar': r, 'Z': 1, 'mu': mu, 'L': 1, 'pi': 0, - 'rho_s': rho_s, 'labor_mkt': ss["N_E"] - 1, 'nA': nA, 'nS': nS, 'B_Y': B_Y, 'sigma_s': sigma_s, - 'goods_mkt': 1 - ss["C"], 'amax': amax, 'asset_mkt': ss["A"] - B, 'nkpc_res': kappa * (w - 1 / mu)}) - - return ss diff --git a/src/sequence_jacobian/models/krusell_smith.py b/src/sequence_jacobian/models/krusell_smith.py deleted file mode 100644 index e6efe0a..0000000 --- a/src/sequence_jacobian/models/krusell_smith.py +++ /dev/null @@ -1,137 +0,0 @@ -import numpy as np -import scipy.optimize as opt - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het -from ..classes.steady_state_dict import SteadyStateDict - - -'''Part 1: HA block''' - - -def household_init(a_grid, e_grid, r, w, eis): - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - Va = (1 + r) * (0.1 * coh) ** (-1 / eis) - return Va - - -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, a_grid, e_grid, r, w, beta, eis): - """Single backward iteration step using endogenous gridpoint method for households with CRRA utility. - - Parameters - ---------- - Va_p : array (S*A), marginal value of assets tomorrow - Pi_p : array (S*S), Markov matrix for skills tomorrow - a_grid : array (A), asset grid - e_grid : array (A), skill grid - r : scalar, ex-post real interest rate - w : scalar, wage - beta : scalar, discount rate today - eis : scalar, elasticity of intertemporal substitution - - Returns - ---------- - Va : array (S*A), marginal value of assets today - a : array (S*A), asset policy today - c : array (S*A), consumption policy today - """ - uc_nextgrid = beta * Va_p - c_nextgrid = uc_nextgrid ** (-eis) - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] - a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) - utils.optimized_routines.setmin(a, a_grid[0]) - c = coh - a - Va = (1 + r) * c ** (-1 / eis) - return Va, a, c - - -'''Part 2: Simple Blocks''' - - -@simple -def firm(K, L, Z, alpha, delta): - r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta - w = (1 - alpha) * Z * (K(-1) / L) ** alpha - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return r, w, Y - - -@simple -def mkt_clearing(K, A, Y, C, delta): - asset_mkt = A - K - goods_mkt = Y - C - delta * K - return asset_mkt, goods_mkt - - -@simple -def income_state_vars(rho, sigma, nS): - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - return e_grid, Pi - - -@simple -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@simple -def firm_steady_state_solution(r, Y, L, delta, alpha): - rk = r + delta - w = (1 - alpha) * Y / L - K = alpha * Y / rk - Z = Y / K ** alpha / L ** (1 - alpha) - return w, K, Z - - -'''Part 3: Steady state''' - - -def ks_ss(lb=0.98, ub=0.999, r=0.01, eis=1, delta=0.025, alpha=0.11, rho=0.966, sigma=0.5, - nS=7, nA=500, amax=200): - """Solve steady state of full GE model. Calibrate beta to hit target for interest rate.""" - # set up grid - a_grid = utils.discretize.agrid(amax=amax, n=nA) - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - - # solve for aggregates analytically - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * (alpha * Z / rk) ** (alpha / (1 - alpha)) - - calibration = {'Pi': Pi, 'a_grid': a_grid, 'e_grid': e_grid, 'r': r, 'w': w, 'eis': eis} - - # solve for beta consistent with this - beta_min = lb / (1 + r) - beta_max = ub / (1 + r) - def res(beta_loc): - calibration['beta'] = beta_loc - return household.steady_state(calibration)['A'] - K - - beta, sol = opt.brentq(res, beta_min, beta_max, full_output=True) - calibration['beta'] = beta - - if not sol.converged: - raise ValueError('Steady-state solver did not converge.') - - # extra evaluation to report variables - ss = household.steady_state(calibration) - ss.update({'Z': Z, 'K': K, 'L': 1.0, 'Y': Y, 'alpha': alpha, 'delta': delta, 'Pi': Pi, - 'goods_mkt': Y - ss['C'] - delta * K, 'nA': nA, 'amax': amax, 'sigma': sigma, - 'rho': rho, 'nS': nS, 'asset_mkt': ss['A'] - K}) - - return ss - - -'''Part 4: Permanent beta heterogeneity''' - - -@simple -def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): - C = mass_patient * C_patient + (1 - mass_patient) * C_impatient - A = mass_patient * A_patient + (1 - mass_patient) * A_impatient - return C, A diff --git a/src/sequence_jacobian/models/rbc.py b/src/sequence_jacobian/models/rbc.py deleted file mode 100644 index 3e44096..0000000 --- a/src/sequence_jacobian/models/rbc.py +++ /dev/null @@ -1,94 +0,0 @@ -import numpy as np - -from ..blocks.simple_block import simple - -'''Part 1: Simple blocks''' - - -@simple -def firm(K, L, Z, alpha, delta): - r = alpha * Z * (K(-1) / L) ** (alpha-1) - delta - w = (1 - alpha) * Z * (K(-1) / L) ** alpha - Y = Z * K(-1) ** alpha * L ** (1 - alpha) - return r, w, Y - - -@simple -def household(K, L, w, eis, frisch, vphi, delta): - C = (w / vphi / L ** (1 / frisch)) ** eis - I = K - (1 - delta) * K(-1) - return C, I - - -@simple -def mkt_clearing(r, C, Y, I, K, L, w, eis, beta): - goods_mkt = Y - C - I - euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - walras = C + K - (1 + r) * K(-1) - w * L - return goods_mkt, euler, walras - - -@simple -def steady_state_solution(Y, L, r, eis, delta, alpha, frisch): - # 1. Solve for beta to hit r - beta = 1 / (1 + r) - - # 2. Solve for K to hit goods_mkt - K = alpha * Y / (r + delta) - w = (1 - alpha) * Y / L - C = w * L + (1 + r) * K(-1) - K - I = delta * K - goods_mkt = Y - C - I - - # 3. Solve for Z to hit Y - Z = Y * K ** (-alpha) * L ** (alpha - 1) - - # 4. Solve for vphi to hit L - vphi = w * C ** (-1 / eis) * L ** (-1 / frisch) - - # 5. Have to return euler because it's a target - euler = C ** (-1 / eis) - beta * (1 + r(+1)) * C(+1) ** (-1 / eis) - - return beta, K, w, C, I, goods_mkt, Z, vphi, euler - - -'''Part 2: Steady state''' - - -def rbc_ss(r=0.01, eis=1, frisch=1, delta=0.025, alpha=0.11): - """Solve steady state of simple RBC model. - - Parameters - ---------- - r : scalar, real interest rate - eis : scalar, elasticity of intertemporal substitution (1/sigma) - frisch : scalar, Frisch elasticity (1/nu) - delta : scalar, depreciation rate - alpha : scalar, capital share - - Returns - ------- - ss : dict, steady state values - """ - # solve for aggregates analytically - rk = r + delta - Z = (rk / alpha) ** alpha # normalize so that Y=1 - K = (alpha * Z / rk) ** (1 / (1 - alpha)) - Y = Z * K ** alpha - w = (1 - alpha) * Z * K ** alpha - I = delta * K - C = Y - I - - # preference params - beta = 1 / (1 + r) - vphi = w * C ** (-1 / eis) - - # check Walras's law, goods market clearing, and the euler equation - walras = C - r * K - w - goods_mkt = Y - C - I - euler = C ** (-1 / eis) - beta * (1 + r) * C ** (-1 / eis) - assert np.abs(walras) < 1E-12 - - return {'beta': beta, 'eis': eis, 'frisch': frisch, 'vphi': vphi, 'delta': delta, 'alpha': alpha, - 'Z': Z, 'K': K, 'I': I, 'Y': Y, 'L': 1, 'C': C, 'w': w, 'r': r, 'walras': walras, 'euler': euler, - 'goods_mkt': goods_mkt} diff --git a/src/sequence_jacobian/models/two_asset.py b/src/sequence_jacobian/models/two_asset.py deleted file mode 100644 index 14f12ac..0000000 --- a/src/sequence_jacobian/models/two_asset.py +++ /dev/null @@ -1,424 +0,0 @@ -# pylint: disable=E1120 -import numpy as np -from numba import guvectorize - -from .. import utilities as utils -from ..blocks.simple_block import simple -from ..blocks.het_block import het -from ..blocks.solved_block import solved -from ..blocks.support.simple_displacement import apply_function -from ..blocks.combined_block import combine - -'''Part 1: HA block''' - - -def household_init(b_grid, a_grid, e_grid, eis, tax, w): - z_grid = income(e_grid, tax, w, 1) - Va = (0.6 + 1.1 * b_grid[:, np.newaxis] + a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - Vb = (0.5 + b_grid[:, np.newaxis] + 1.2 * a_grid) ** (-1 / eis) * np.ones((z_grid.shape[0], 1, 1)) - - return z_grid, Va, Vb - - -@het(exogenous='Pi', policy=['b', 'a'], backward=['Vb', 'Va'], backward_init=household_init) # order as in grid! -def household(Va_p, Vb_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, ra, chi0, chi1, chi2): - # require that k is decreasing (new) - assert k_grid[1] < k_grid[0], 'kappas in k_grid must be decreasing!' - - # precompute Psi1(a', a) on grid of (a', a) for steps 3 and 5 - Psi1 = get_Psi_and_deriv(a_grid[:, np.newaxis], - a_grid[np.newaxis, :], ra, chi0, chi1, chi2)[1] - - # === STEP 2: Wb(z, b', a') and Wa(z, b', a') === - # (take discounted expectation of tomorrow's value function) - Wb = beta * Vb_p - Wa = beta * Va_p - W_ratio = Wa / Wb - - # === STEP 3: a'(z, b', a) for UNCONSTRAINED === - - # for each (z, b', a), linearly interpolate to find a' between gridpoints - # satisfying optimality condition W_ratio == 1+Psi1 - i, pi = lhs_equals_rhs_interpolate(W_ratio, 1 + Psi1) - - # use same interpolation to get Wb and then c - a_endo_unc = utils.interpolate.apply_coord(i, pi, a_grid) - c_endo_unc = utils.interpolate.apply_coord(i, pi, Wb) ** (-eis) - - # === STEP 4: b'(z, b, a), a'(z, b, a) for UNCONSTRAINED === - - # solve out budget constraint to get b(z, b', a) - b_endo = (c_endo_unc + a_endo_unc + addouter(-z_grid, b_grid, -(1 + ra) * a_grid) - + get_Psi_and_deriv(a_endo_unc, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) - - # interpolate this b' -> b mapping to get b -> b', so we have b'(z, b, a) - # and also use interpolation to get a'(z, b, a) - # (note utils.interpolate.interpolate_coord and utils.interpolate.apply_coord work on last axis, - # so we need to swap 'b' to the last axis, then back when done) - i, pi = utils.interpolate.interpolate_coord(b_endo.swapaxes(1, 2), b_grid) - a_unc = utils.interpolate.apply_coord(i, pi, a_endo_unc.swapaxes(1, 2)).swapaxes(1, 2) - b_unc = utils.interpolate.apply_coord(i, pi, b_grid).swapaxes(1, 2) - - # === STEP 5: a'(z, kappa, a) for CONSTRAINED === - - # for each (z, kappa, a), linearly interpolate to find a' between gridpoints - # satisfying optimality condition W_ratio/(1+kappa) == 1+Psi1, assuming b'=0 - lhs_con = W_ratio[:, 0:1, :] / (1 + k_grid[np.newaxis, :, np.newaxis]) - i, pi = lhs_equals_rhs_interpolate(lhs_con, 1 + Psi1) - - # use same interpolation to get Wb and then c - a_endo_con = utils.interpolate.apply_coord(i, pi, a_grid) - c_endo_con = ((1 + k_grid[np.newaxis, :, np.newaxis]) ** (-eis) - * utils.interpolate.apply_coord(i, pi, Wb[:, 0:1, :]) ** (-eis)) - - # === STEP 6: a'(z, b, a) for CONSTRAINED === - - # solve out budget constraint to get b(z, kappa, a), enforcing b'=0 - b_endo = (c_endo_con + a_endo_con - + addouter(-z_grid, np.full(len(k_grid), b_grid[0]), -(1 + ra) * a_grid) - + get_Psi_and_deriv(a_endo_con, a_grid, ra, chi0, chi1, chi2)[0]) / (1 + rb) - - # interpolate this kappa -> b mapping to get b -> kappa - # then use the interpolated kappa to get a', so we have a'(z, b, a) - # (utils.interpolate.interpolate_y does this in one swoop, but since it works on last - # axis, we need to swap kappa to last axis, and then b back to middle when done) - a_con = utils.interpolate.interpolate_y(b_endo.swapaxes(1, 2), b_grid, - a_endo_con.swapaxes(1, 2)).swapaxes(1, 2) - - # === STEP 7: obtain policy functions and update derivatives of value function === - - # combine unconstrained solution and constrained solution, choosing latter - # when unconstrained goes below minimum b - a, b = a_unc.copy(), b_unc.copy() - b[b <= b_grid[0]] = b_grid[0] - a[b <= b_grid[0]] = a_con[b <= b_grid[0]] - - # calculate adjustment cost and its derivative - Psi, _, Psi2 = get_Psi_and_deriv(a, a_grid, ra, chi0, chi1, chi2) - - # solve out budget constraint to get consumption and marginal utility - c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b - uc = c ** (-1 / eis) - - # for GE wage Phillips curve we'll need endowment-weighted utility too - u = e_grid[:, np.newaxis, np.newaxis] * uc - - # update derivatives of value function using envelope conditions - Va = (1 + ra - Psi2) * uc - Vb = (1 + rb) * uc - - return Va, Vb, a, b, c, u - - -def income(e_grid, tax, w, N): - z_grid = (1 - tax) * w * N * e_grid - return z_grid - - -# A potential hetoutput to include with the above HetBlock -def adjustment_costs(a, a_grid, ra, chi0, chi1, chi2): - chi, _, _ = apply_function(get_Psi_and_deriv, a, a_grid, ra, chi0, chi1, chi2) - return chi - - -household = household.add_hetinputs([income]) -household = household.add_hetoutputs([adjustment_costs]) - - -"""Supporting functions for HA block""" - - -def get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2): - """Adjustment cost Psi(ap, a) and its derivatives with respect to - first argument (ap) and second argument (a)""" - a_with_return = (1 + ra) * a - a_change = ap - a_with_return - abs_a_change = np.abs(a_change) - sign_change = np.sign(a_change) - - adj_denominator = a_with_return + chi0 - core_factor = (abs_a_change / adj_denominator) ** (chi2 - 1) - - Psi = chi1 / chi2 * abs_a_change * core_factor - Psi1 = chi1 * sign_change * core_factor - Psi2 = -(1 + ra) * (Psi1 + (chi2 - 1) * Psi / adj_denominator) - return Psi, Psi1, Psi2 - - -def matrix_times_first_dim(A, X): - """Take matrix A times vector X[:, i1, i2, i3, ... , in] separately - for each i1, i2, i3, ..., in. Same output as A @ X if X is 1D or 2D""" - # flatten all dimensions of X except first, then multiply, then restore shape - return (A @ X.reshape(X.shape[0], -1)).reshape(X.shape) - - -def addouter(z, b, a): - """Take outer sum of three arguments: result[i, j, k] = z[i] + b[j] + a[k]""" - return z[:, np.newaxis, np.newaxis] + b[:, np.newaxis] + a - - -@guvectorize(['void(float64[:], float64[:,:], uint32[:], float64[:])'], '(ni),(ni,nj)->(nj),(nj)') -def lhs_equals_rhs_interpolate(lhs, rhs, iout, piout): - """ - Given lhs (i) and rhs (i,j), for each j, find the i such that - - lhs[i] > rhs[i,j] and lhs[i+1] < rhs[i+1,j] - - i.e. where given j, lhs == rhs in between i and i+1. - - Also return the pi such that - - pi*(lhs[i] - rhs[i,j]) + (1-pi)*(lhs[i+1] - rhs[i+1,j]) == 0 - - i.e. such that the point at pi*i + (1-pi)*(i+1) satisfies lhs == rhs by linear interpolation. - - If lhs[0] < rhs[0,j] already, just return u=0 and pi=1. - - ***IMPORTANT: Assumes that solution i is monotonically increasing in j - and that lhs - rhs is monotonically decreasing in i.*** - """ - - ni, nj = rhs.shape - assert len(lhs) == ni - - i = 0 - for j in range(nj): - while True: - if lhs[i] < rhs[i, j]: - break - elif i < nj - 1: - i += 1 - else: - break - - if i == 0: - iout[j] = 0 - piout[j] = 1 - else: - iout[j] = i - 1 - err_upper = rhs[i, j] - lhs[i] - err_lower = rhs[i - 1, j] - lhs[i - 1] - piout[j] = err_upper / (err_upper - err_lower) - - -'''Part 2: Simple blocks''' - - -@simple -def pricing(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) \ - / (1 + r(+1)) - (1 + pi).apply(np.log) - return nkpc - - -@simple -def arbitrage(div, p, r): - equity = div(+1) + p(+1) - p * (1 + r(+1)) - return equity - - -@simple -def labor(Y, w, K, Z, alpha): - N = (Y / Z / K(-1) ** alpha) ** (1 / (1 - alpha)) - mc = w * N / (1 - alpha) / Y - return N, mc - - -@simple -def investment(Q, K, r, N, mc, Z, delta, epsI, alpha): - inv = (K / K(-1) - 1) / (delta * epsI) + 1 - Q - val = alpha * Z(+1) * (N(+1) / K) ** (1 - alpha) * mc(+1) - (K(+1) / K - (1 - delta) + (K(+1) / K - 1) ** 2 / ( - 2 * delta * epsI)) + K(+1) / K * Q(+1) - (1 + r(+1)) * Q - return inv, val - - -@simple -def dividend(Y, w, N, K, pi, mup, kappap, delta, epsI): - psip = mup / (mup - 1) / 2 / kappap * (1 + pi).apply(np.log) ** 2 * Y - k_adjust = K(-1) * (K / K(-1) - 1) ** 2 / (2 * delta * epsI) - I = K - (1 - delta) * K(-1) + k_adjust - div = Y - w * N - I - psip - return psip, I, div - - -@simple -def taylor(rstar, pi, phi): - i = rstar + phi * pi - return i - - -@simple -def fiscal(r, w, N, G, Bg): - tax = (r * Bg + G) / w / N - return tax - - -@simple -def finance(i, p, pi, r, div, omega, pshare): - rb = r - omega - ra = pshare(-1) * (div + p) / p(-1) + (1 - pshare(-1)) * (1 + r) - 1 - fisher = 1 + i(-1) - (1 + r) * (1 + pi) - return rb, ra, fisher - - -@simple -def wage(pi, w): - piw = (1 + pi) * w / w(-1) - 1 - return piw - - -@simple -def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ - (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) - return wnkpc - - -@simple -def mkt_clearing(p, A, B, Bg, C, I, G, CHI, psip, omega, Y): - wealth = A + B - asset_mkt = p + Bg - wealth - goods_mkt = C + I + G + CHI + psip + omega * B - Y - return asset_mkt, wealth, goods_mkt - - -@simple -def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): - b_grid = utils.discretize.agrid(amax=bmax, n=nB) - a_grid = utils.discretize.agrid(amax=amax, n=nA) - k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) - - return b_grid, a_grid, k_grid, e_grid, Pi - - -@simple -def share_value(p, tot_wealth, Bh): - pshare = p / (tot_wealth - Bh) - return pshare - - -@simple -def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): - """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" - # 1. Solve for markup to hit total wealth - p = tot_wealth - Bg - mc = 1 - r * (p - K) / Y - mup = 1 / mc - wealth = tot_wealth - - # 2. Solve for capital share to hit K - alpha = (r + delta) * K / Y / mc - - # 3. Solve for TFP to hit N (Y cannot be used, because it is an unknown of the DAG) - Z = Y * K ** (-alpha) * N ** (alpha - 1) - - # 4. Solve for w such that piw = 0 - w = mc * (1 - alpha) * Y / N - piw = 0 - - return p, mc, mup, wealth, alpha, Z, w, piw - - -@simple -def partial_ss_step2(tax, w, U, N, muw, frisch): - """Solves for (vphi) to hit (wnkpc).""" - vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) - wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw - return vphi, wnkpc - - -'''Part 3: Steady state''' - - -def two_asset_ss(beta_guess=0.976, chi1_guess=6.5, r=0.0125, tot_wealth=14, K=10, delta=0.02, kappap=0.1, - muw=1.1, Bh=1.04, Bg=2.8, G=0.2, eis=0.5, frisch=1, chi0=0.25, chi2=2, epsI=4, omega=0.005, kappaw=0.1, - phi=1.5, nZ=3, nB=50, nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92, - tol=1e-12, verbose=True): - """Solve steady state of full GE model. Calibrate (beta, vphi, chi1, alpha, mup, Z) to hit targets for - (r, tot_wealth, Bh, K, Y=N=1). - """ - - # set up grid - b_grid = utils.discretize.agrid(amax=bmax, n=nB) - a_grid = utils.discretize.agrid(amax=amax, n=nA) - k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) - - # solve analytically what we can - I = delta * K - mc = 1 - r * (tot_wealth - Bg - K) - alpha = (r + delta) * K / mc - mup = 1 / mc - Z = K ** (-alpha) - w = (1 - alpha) * mc - tax = (r * Bg + G) / w - div = 1 - w - I - p = div / r - ra = r - rb = r - omega - - # figure out initializer - calibration = {'Pi': Pi, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, - 'N': 1.0, 'tax': tax, 'w': w, 'eis': eis, 'rb': rb, 'ra': ra, 'chi0': chi0, 'chi2': chi2} - - # residual function - def res(x): - beta_loc, chi1_loc = x - - if beta_loc > 0.999 / (1 + r) or chi1_loc < 0.5: - raise ValueError('Clearly invalid inputs') - - calibration['beta'], calibration['chi1'] = beta_loc, chi1_loc - out = household.steady_state(calibration) - asset_mkt = out['A'] + out['B'] - p - Bg - return np.array([asset_mkt, out['B'] - Bh]) - - # solve for beta, vphi, omega - (beta, chi1), _ = utils.solvers.broyden_solver(res, np.array([beta_guess, chi1_guess]), tol=tol, verbose=verbose) - calibration['beta'], calibration['chi1'] = beta, chi1 - - # extra evaluation for reporting - ss = household.steady_state(calibration) - - # other things of interest - vphi = (1 - tax) * w * ss['U'] / muw - pshare = p / (tot_wealth - Bh) - - # calculate aggregate adjustment cost and check Walras's law - chi = get_Psi_and_deriv(ss.internals["household"]['a'], a_grid, r, chi0, chi1, chi2)[0] - Chi = np.vdot(ss.internals["household"]['D'], chi) - goods_mkt = ss['C'] + I + G + Chi + omega * ss['B'] - 1 - - ss.internals["household"].update({"chi": chi}) - ss.update({'pi': 0, 'piw': 0, 'Q': 1, 'Y': 1, 'N': 1, 'mc': mc, 'K': K, 'Z': Z, 'I': I, 'w': w, 'tax': tax, - 'div': div, 'p': p, 'r': r, 'Bg': Bg, 'G': G, 'CHI': Chi, 'phi': phi, 'wealth': tot_wealth, - 'beta': beta, 'vphi': vphi, 'omega': omega, 'alpha': alpha, 'delta': delta, 'mup': mup, 'muw': muw, - 'frisch': frisch, 'epsI': epsI, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, - 'k_grid': k_grid, 'Pi': Pi, 'kappap': kappap, 'kappaw': kappaw, 'pshare': pshare, 'rstar': r, 'i': r, - 'tot_wealth': tot_wealth, 'fisher': 0, 'nZ': nZ, 'Bh': Bh, 'psip': 0, 'inv': 0, 'goods_mkt': goods_mkt, - 'equity': div + p - p * (1 + r), 'bmax': bmax, 'rho_z': rho_z, 'asset_mkt': p + Bg - ss["B"] - ss["A"], - 'nA': nA, 'nB': nB, 'amax': amax, 'kmax': kmax, 'nK': nK, 'nkpc': kappap * (mc - 1 / mup), - 'wnkpc': kappaw * (vphi * ss["N"] ** (1 + 1 / frisch) - (1 - tax) * w * ss["N"] * ss["U"] / muw), - 'sigma_z': sigma_z, 'val': alpha * Z * (ss["N"] / K) ** (1 - alpha) * mc - delta - r}) - return ss - - -'''Part 4: Solved blocks for transition dynamics/Jacobian calculation''' - - -@solved(unknowns={'pi': (-0.1, 0.1)}, targets=['nkpc'], solver="brentq") -def pricing_solved(pi, mc, r, Y, kappap, mup): - nkpc = kappap * (mc - 1 / mup) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / \ - (1 + r(+1)) - (1 + pi).apply(np.log) - return nkpc - - -@solved(unknowns={'p': (5, 15)}, targets=['equity'], solver="brentq") -def arbitrage_solved(div, p, r): - equity = div(+1) + p(+1) - p * (1 + r(+1)) - return equity - - -production = combine([labor, investment]) -production_solved = production.solved(unknowns={'Q': 1., 'K': 10.}, targets=['inv', 'val'], solver="broyden_custom") diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index a142ed8..bfb3dc1 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -1,8 +1,9 @@ -from sequence_jacobian.utilities.ordered_set import OrderedSet import re import inspect import numpy as np +from sequence_jacobian.utilities.ordered_set import OrderedSet + # TODO: fix this, have it twice (main version in misc) due to circular import problem # let's make everything point to here for input_list, etc. so that this is unnecessary def make_tuple(x): @@ -189,7 +190,7 @@ def wrapped_call(self, input_dict, preprocess=None, postprocess=None): raise NotImplementedError def add(self, f): - if isinstance(f, function) or isinstance(f, ExtendedFunction): + if inspect.isfunction(f) or isinstance(f, ExtendedFunction): return ExtendedParallelFunction(list(self.functions.values()) + [f]) else: # otherwise assume f is iterable diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 5c3711e..6bf589c 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -4,15 +4,12 @@ import numpy as np from sequence_jacobian import estimation -from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset -from sequence_jacobian import create_model - # See test_determinacy.py for the to-do describing this suppression @pytest.mark.filterwarnings("ignore:.*cannot be safely interpreted as an integer.*:DeprecationWarning") def test_krusell_smith_estimation(krusell_smith_dag): - ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag + ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag np.random.seed(41234) T = 50 diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 9ac3703..2fc8d98 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -3,8 +3,8 @@ import numpy as np def test_ks_jac(krusell_smith_dag): - ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag - household, firm, mkt_clearing, _, _ = ks_model._blocks_unsorted + ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag + household, firm, *_ = ks_model._blocks_unsorted T = 10 # Automatically calculate the general equilibrium Jacobian @@ -56,7 +56,7 @@ def test_ks_jac(krusell_smith_dag): def test_fake_news_v_direct_method(one_asset_hank_dag): - hank_model, _, _, _, ss = one_asset_hank_dag + hank_model, ss, *_ = one_asset_hank_dag household = hank_model['household'] T = 40 diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index aef2a77..773648c 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -3,13 +3,13 @@ import numpy as np from sequence_jacobian import combine -from sequence_jacobian.models import rbc +from sequence_jacobian.examples import rbc from sequence_jacobian.blocks.auxiliary_blocks.jacobiandict_block import JacobianDictBlock from sequence_jacobian import SteadyStateDict def test_jacobian_dict_block_impulses(rbc_dag): - rbc_model, exogenous, unknowns, _, ss = rbc_dag + rbc_model, ss, unknowns, _, exogenous = rbc_dag T = 10 J_pe = rbc_model.jacobian(ss, inputs=unknowns + exogenous, T=10) @@ -29,7 +29,7 @@ def test_jacobian_dict_block_impulses(rbc_dag): def test_jacobian_dict_block_combine(rbc_dag): - _, exogenous, _, _, ss = rbc_dag + _, ss, _, _, exogenous = rbc_dag J_firm = rbc.firm.jacobian(ss, inputs=exogenous) blocks_w_jdict = [rbc.household, J_firm, rbc.mkt_clearing] diff --git a/tests/base/test_options.py b/tests/base/test_options.py index 5cac169..67456d3 100644 --- a/tests/base/test_options.py +++ b/tests/base/test_options.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from sequence_jacobian.models import krusell_smith +from sequence_jacobian.examples import krusell_smith def test_jacobian_h(krusell_smith_dag): - dag, *_, ss = krusell_smith_dag + dag, ss, *_ = krusell_smith_dag hh = dag['household'] lowacc = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, h=0.05) @@ -26,8 +26,9 @@ def test_jacobian_h(krusell_smith_dag): def test_jacobian_steady_state(krusell_smith_dag): dag = krusell_smith_dag[0] - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, - "nS": 2, "nA": 10, "amax": 200, "r": 0.01, 'beta': 0.96, "Z": 0.85, "K": 3.} + calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, + "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01, 'beta': 0.96, + "Z": 0.85, "K": 3.} pytest.raises(ValueError, dag.steady_state, calibration, options={'household': {'backward_maxit': 10}}) @@ -36,25 +37,16 @@ def test_jacobian_steady_state(krusell_smith_dag): assert ss1['A'] == ss2['A'] - def test_steady_state_solution(krusell_smith_dag): - dag, _, unknowns, targets, _ = krusell_smith_dag - helper_blocks = [krusell_smith.firm_steady_state_solution] + dag, *_ = krusell_smith_dag + helper_blocks = [krusell_smith.firm_ss_solution] - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, - "nS": 2, "nA": 10, "amax": 200, "r": 0.01} + calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, + "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01} unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} - pytest.raises(RuntimeError, dag.solve_steady_state, calibration, unknowns_ss, targets_ss, solver="brentq", - helper_blocks=helper_blocks, helper_targets=["Y", "r"], ttol=1E-2, ctol=1E-9) - - # ss = dag.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", - # helper_blocks=helper_blocks, helper_targets=["Y", "r"], verbose=False) - - # assert not capsys.readouterr().out - - # ss = dag.solve_steady_state(calibration, {**unknowns_ss, 'beta':0.98}, targets_ss, solver="newton_custom", - # helper_blocks=helper_blocks, helper_targets=["Y", "r"]) - - # assert capsys.readouterr().out + pytest.raises(RuntimeError, dag.solve_steady_state, calibration, + unknowns_ss, targets_ss, solver="brentq", + helper_blocks=helper_blocks, helper_targets=["Y", "r"], + ttol=1E-2, ctol=1E-9) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index d7ca66c..d3509e7 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -9,7 +9,7 @@ from sequence_jacobian.utilities.bijection import Bijection def test_impulsedict(krusell_smith_dag): - ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag + ks_model, ss, unknowns, targets, _ = krusell_smith_dag T = 200 # Linearized impulse responses as deviations diff --git a/tests/base/test_steady_state.py b/tests/base/test_steady_state.py index 2a412e1..8fa19aa 100644 --- a/tests/base/test_steady_state.py +++ b/tests/base/test_steady_state.py @@ -2,42 +2,42 @@ import numpy as np -from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset +from sequence_jacobian.examples import rbc, krusell_smith, hank, two_asset -def test_rbc_steady_state(rbc_dag): - _, _, _, _, ss = rbc_dag - ss_ref = rbc.rbc_ss() - assert set(ss.keys()) == set(ss_ref.keys()) - for k in ss.keys(): - assert np.all(np.isclose(ss[k], ss_ref[k])) +# def test_rbc_steady_state(rbc_dag): +# _, ss, *_ = rbc_dag +# ss_ref = rbc.rbc_ss() +# assert set(ss.keys()) == set(ss_ref.keys()) +# for k in ss.keys(): +# assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_ks_steady_state(krusell_smith_dag): - _, _, _, _, ss = krusell_smith_dag - ss_ref = krusell_smith.ks_ss(nS=2, nA=10, amax=200) - assert set(ss.keys()) == set(ss_ref.keys()) - for k in ss.keys(): - assert np.all(np.isclose(ss[k], ss_ref[k])) +# def test_ks_steady_state(krusell_smith_dag): +# _, ss, *_ = krusell_smith_dag +# ss_ref = krusell_smith.ks_ss(nS=2, nA=10, amax=200) +# assert set(ss.keys()) == set(ss_ref.keys()) +# for k in ss.keys(): +# assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_hank_steady_state(one_asset_hank_dag): - _, _, _, _, ss = one_asset_hank_dag - ss_ref = hank.hank_ss(nS=2, nA=10, amax=150) - assert set(ss.keys()) == set(ss_ref.keys()) - for k in ss.keys(): - assert np.all(np.isclose(ss[k], ss_ref[k])) +# def test_hank_steady_state(one_asset_hank_dag): +# _, ss, *_ = one_asset_hank_dag +# ss_ref = hank.hank_ss(nS=2, nA=10, amax=150) +# assert set(ss.keys()) == set(ss_ref.keys()) +# for k in ss.keys(): +# assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_two_asset_steady_state(two_asset_hank_dag): - _, _, _, _, ss = two_asset_hank_dag - ss_ref = two_asset.two_asset_ss(nZ=3, nB=10, nA=16, nK=4, verbose=False) - assert set(ss.keys()) == set(ss_ref.keys()) - for k in ss.keys(): - assert np.all(np.isclose(ss[k], ss_ref[k])) +# def test_two_asset_steady_state(two_asset_hank_dag): +# _, ss, *_ = two_asset_hank_dag +# ss_ref = two_asset.two_asset_ss(nZ=3, nB=10, nA=16, nK=4, verbose=False) +# assert set(ss.keys()) == set(ss_ref.keys()) +# for k in ss.keys(): +# assert np.all(np.isclose(ss[k], ss_ref[k])) -def test_remap_steady_state(ks_remapped_dag): - _, _, _, _, ss = ks_remapped_dag - assert ss['beta_impatient'] < ss['beta_patient'] - assert ss['A_impatient'] < ss['A_patient'] +# def test_remap_steady_state(ks_remapped_dag): +# _, _, _, _, ss = ks_remapped_dag +# assert ss['beta_impatient'] < ss['beta_patient'] +# assert ss['A_impatient'] < ss['A_patient'] diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index a3386c6..308ca48 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -3,13 +3,14 @@ import numpy as np from sequence_jacobian import combine -from sequence_jacobian.models import two_asset +from sequence_jacobian.examples import two_asset +from sequence_jacobian.examples.hetblocks import household_twoasset as hh # TODO: Figure out a more robust way to check similarity of the linear and non-linear solution. # As of now just checking that the tolerance for difference (by infinity norm) is below a manually checked threshold def test_rbc_td(rbc_dag): - rbc_model, exogenous, unknowns, targets, ss = rbc_dag + rbc_model, ss, unknowns, targets, exogenous = rbc_dag T, impact, rho, news = 30, 0.01, 0.8, 10 G = rbc_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) @@ -30,7 +31,7 @@ def test_rbc_td(rbc_dag): def test_ks_td(krusell_smith_dag): - ks_model, exogenous, unknowns, targets, ss = krusell_smith_dag + ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag T = 30 G = ks_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) @@ -46,7 +47,7 @@ def test_ks_td(krusell_smith_dag): def test_hank_td(one_asset_hank_dag): - hank_model, exogenous, unknowns, targets, ss = one_asset_hank_dag + hank_model, ss, unknowns, targets, exogenous = one_asset_hank_dag T = 30 household = hank_model._blocks_unsorted[0] @@ -66,7 +67,7 @@ def test_hank_td(one_asset_hank_dag): # TODO: needs to compute Jacobian of hetoutput `Chi` def test_two_asset_td(two_asset_hank_dag): - two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag + two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag T = 30 household = two_asset_model._blocks_unsorted[0] @@ -86,12 +87,14 @@ def test_two_asset_td(two_asset_hank_dag): def test_two_asset_solved_v_simple_td(two_asset_hank_dag): - two_asset_model, exogenous, unknowns, targets, ss = two_asset_hank_dag - - blocks_simple = [two_asset.household, two_asset.make_grids, - two_asset.pricing, two_asset.arbitrage, two_asset.labor, two_asset.investment, - two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, - two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] + two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag + + household = hh.household.add_hetinputs([two_asset.income]) + blocks_simple = [household, two_asset.make_grids, two_asset.pricing, two_asset.arbitrage, + two_asset.labor, two_asset.investment, two_asset.dividend, + two_asset.taylor, two_asset.fiscal, two_asset.share_value, + two_asset.finance, two_asset.wage, two_asset.union, + two_asset.mkt_clearing] two_asset_model_simple = combine(blocks_simple, name="Two-Asset HANK w/ SimpleBlocks") unknowns_simple = ["r", "w", "Y", "pi", "p", "Q", "K"] targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] diff --git a/tests/base/test_two_asset.py b/tests/base/test_two_asset.py index 94ce995..8aba5f2 100644 --- a/tests/base/test_two_asset.py +++ b/tests/base/test_two_asset.py @@ -2,7 +2,7 @@ import numpy as np -from sequence_jacobian.models import two_asset +from sequence_jacobian.examples.hetblocks import household_twoasset as hh from sequence_jacobian import utilities as utils @@ -13,9 +13,9 @@ def test_hank_ss(): assert np.isclose(U, 4.5102870939550055) -def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg=2.8, G=0.2, eis=0.5, - chi0=0.25, chi1=6.5, chi2=2, omega=0.005, nZ=3, nB=50, nA=70, nK=50, - bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92): +def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg=2.8, G=0.2, + eis=0.5, chi0=0.25, chi1=6.5, chi2=2, omega=0.005, nZ=3, nB=50, + nA=70, nK=50, bmax=50, amax=4000, kmax=1, rho_z=0.966, sigma_z=0.92): """Mostly cribbed from two_asset.hank_ss(), but just does backward iteration to get a partial equilibrium household steady state given parameters, not solving for equilibrium. Convenient for testing.""" @@ -24,23 +24,24 @@ def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg= b_grid = utils.discretize.agrid(amax=bmax, n=nB) a_grid = utils.discretize.agrid(amax=amax, n=nA) k_grid = utils.discretize.agrid(amax=kmax, n=nK)[::-1].copy() - e_grid, pi, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho_z, sigma=sigma_z, N=nZ) # solve analytically what we can - I = delta * K mc = 1 - r * (tot_wealth - Bg - K) alpha = (r + delta) * K / mc w = (1 - alpha) * mc tax = (r * Bg + G) / w ra = r rb = r - omega + z_grid = (1 - tax) * w * e_grid # figure out initializer - calibration = {'Pi': Pi, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, 'k_grid': k_grid, - 'beta': beta, 'N': 1.0, 'tax': tax, 'w': w, 'eis': eis, 'rb': rb, 'ra': ra, + calibration = {'Pi': Pi, 'a_grid': a_grid, 'b_grid': b_grid, 'e_grid': e_grid, + 'z_grid': z_grid, 'k_grid': k_grid, 'beta': beta, 'N': 1.0, + 'tax': tax, 'w': w, 'eis': eis, 'rb': rb, 'ra': ra, 'chi0': chi0, 'chi1': chi1, 'chi2': chi2} - out = two_asset.household.steady_state(calibration) + out = hh.household.steady_state(calibration) return out['A'], out['B'], out['U'] @@ -53,7 +54,7 @@ def test_Psi(): a = np.random.rand(50) + 1 ap = np.random.rand(50) + 1 - oPsi, oPsi1, oPsi2 = two_asset.get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2) + oPsi, oPsi1, oPsi2 = hh.get_Psi_and_deriv(ap, a, ra, chi0, chi1, chi2) Psi = Psi_correct(ap, a, ra, chi0, chi1, chi2) assert np.allclose(oPsi, Psi) diff --git a/tests/conftest.py b/tests/conftest.py index 274d7d4..9e0de9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,126 +2,29 @@ import pytest -from sequence_jacobian import create_model -from sequence_jacobian.models import rbc, krusell_smith, hank, two_asset +from sequence_jacobian.examples import rbc, krusell_smith, hank, two_asset @pytest.fixture(scope='session') def rbc_dag(): - blocks = [rbc.household, rbc.mkt_clearing, rbc.firm] - rbc_model = create_model(blocks, name="RBC") - - # Steady State - calibration = {"eis": 1., "frisch": 1., "delta": 0.025, "alpha": 0.11, "L": 1.} - unknowns_ss = {"vphi": 0.92, "beta": 1 / (1 + 0.01), "K": 2., "Z": 1.} - targets_ss = {"goods_mkt": 0., "r": 0.01, "euler": 0., "Y": 1.} - ss = rbc_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="hybr") - - # Transitional Dynamics/Jacobian Calculation - exogenous = ["Z"] - unknowns = ["K", "L"] - targets = ["goods_mkt", "euler"] - - return rbc_model, exogenous, unknowns, targets, ss + return rbc.dag() @pytest.fixture(scope='session') def krusell_smith_dag(): - blocks = [krusell_smith.household, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars, - krusell_smith.asset_state_vars] - helper_blocks = [krusell_smith.firm_steady_state_solution] - ks_model = create_model(blocks, name="Krusell-Smith") - - # Steady State - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, - "nS": 2, "nA": 10, "amax": 200, "r": 0.01} - unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} - targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} - ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", - helper_blocks=helper_blocks, helper_targets=["Y", "r"]) - - # Transitional Dynamics/Jacobian Calculation - exogenous = ["Z"] - unknowns = ["K"] - targets = ["asset_mkt"] - - return ks_model, exogenous, unknowns, targets, ss + return krusell_smith.dag() @pytest.fixture(scope='session') def one_asset_hank_dag(): - blocks = [hank.household, hank.firm, hank.monetary, hank.fiscal, hank.mkt_clearing, hank.nkpc, - hank.income_state_vars, hank.asset_state_vars] - helper_blocks = [hank.partial_steady_state_solution] - hank_model = create_model(blocks, name="One-Asset HANK") - - # Steady State - calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B_Y": 5.6, "mu": 1.2, - "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, - "pi": 0, "nS": 2, "amax": 150, "nA": 10} - unknowns_ss = {"beta": 0.986, "vphi": 0.8, "w": 0.8} - targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} - ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", - helper_blocks=helper_blocks, helper_targets=["nkpc_res"]) - - # Transitional Dynamics/Jacobian Calculation - exogenous = ["rstar", "Z"] - unknowns = ["pi", "w", "Y"] - targets = ["nkpc_res", "asset_mkt", "labor_mkt"] - - return hank_model, exogenous, unknowns, targets, ss + return hank.dag() @pytest.fixture(scope='session') def two_asset_hank_dag(): - blocks = [two_asset.household, two_asset.make_grids, - two_asset.pricing_solved, two_asset.arbitrage_solved, two_asset.production_solved, - two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, - two_asset.finance, two_asset.wage, two_asset.union, two_asset.mkt_clearing] - helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2] - two_asset_model = create_model(blocks, name="Two-Asset HANK") - - # Steady State - calibration = {"Y": 1., "r": 0.0125, "rstar": 0.0125, "tot_wealth": 14, "delta": 0.02, "kappap": 0.1, "muw": 1.1, - "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, - "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, - "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} - targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} - ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", - helper_blocks=helper_blocks, - helper_targets=["wnkpc", "piw", "K", "wealth", "N"]) - - # Transitional Dynamics/Jacobian Calculation - exogenous = ["rstar", "Z", "G"] - unknowns = ["r", "w", "Y"] - targets = ["asset_mkt", "fisher", "wnkpc"] - - return two_asset_model, exogenous, unknowns, targets, ss + return two_asset.dag() @pytest.fixture(scope='session') def ks_remapped_dag(): - # Create 2 versions of the household block using `remap` - to_map = ['beta', *krusell_smith.household.outputs] - hh_patient = krusell_smith.household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') - hh_impatient = krusell_smith.household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') - blocks = [hh_patient, hh_impatient, krusell_smith.firm, krusell_smith.mkt_clearing, krusell_smith.income_state_vars, - krusell_smith.asset_state_vars, krusell_smith.aggregate] - ks_remapped = create_model(blocks, name='Krusell-Smith') - - # Steady State - calibration = {'eis': 1., 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, - 'nS': 3, 'nA': 100, 'amax': 1000, 'beta_impatient': 0.985, 'mass_patient': 0.5} - unknowns_ss = {'beta_patient': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.5, 'K': 8.} - targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} - helper_blocks = [krusell_smith.firm_steady_state_solution] - ss = ks_remapped.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', - helper_blocks=helper_blocks, helper_targets=['Y', 'r']) - - # Transitional Dynamics/Jacobian Calculation - exogenous = ["Z"] - unknowns = ["K"] - targets = ["asset_mkt"] - - return ks_remapped, exogenous, unknowns, targets, ss + return krusell_smith.remapped_dag() diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index 7176151..d9c113d 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -3,15 +3,14 @@ import pytest import numpy as np -from sequence_jacobian.models import hank, two_asset +from sequence_jacobian.examples import hank, two_asset # Filter out warnings when the solver is trying to search in bad regions @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): - hank_model, _, _, _, ss = one_asset_hank_dag - - helper_blocks = [hank.partial_steady_state_solution] + hank_model, ss, *_ = one_asset_hank_dag + helper_blocks = [hank.partial_ss_solution] calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B_Y": 5.6, "mu": 1.2, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, @@ -28,8 +27,7 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag): - two_asset_model, _, _, _, ss = two_asset_hank_dag - + two_asset_model, ss, *_ = two_asset_hank_dag helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2] # Steady State From 1acf78b18d71d7e7409c540c575137ec4081be15 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 15 Sep 2021 15:43:57 -0400 Subject: [PATCH 272/288] Edited examples: take advantage of hetoutput to streamline backward_fun. More logical helper for HANK1. --- src/sequence_jacobian/examples/hank.py | 58 ++++++++++--------- .../examples/hetblocks/household_labor.py | 17 ++---- .../examples/hetblocks/household_twoasset.py | 7 +-- .../examples/krusell_smith.py | 7 +-- src/sequence_jacobian/examples/two_asset.py | 10 ++-- tests/base/test_two_asset.py | 6 +- 6 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/sequence_jacobian/examples/hank.py b/src/sequence_jacobian/examples/hank.py index 946707b..3085d39 100644 --- a/src/sequence_jacobian/examples/hank.py +++ b/src/sequence_jacobian/examples/hank.py @@ -2,6 +2,7 @@ from .. import utilities as utils from ..blocks.simple_block import simple +from ..blocks.solved_block import solved from ..blocks.combined_block import create_model, combine from .hetblocks import household_labor as hh @@ -22,6 +23,13 @@ def monetary(pi, rstar, phi): return r +@simple +def nkpc(pi, w, Z, Y, r, mu, kappa): + nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ + - (1 + pi).apply(np.log) + return nkpc_res + + @simple def fiscal(r, B): Tax = r * B @@ -29,31 +37,19 @@ def fiscal(r, B): @simple -def mkt_clearing(A, N_E, C, L, Y, B, pi, mu, kappa): +def mkt_clearing(A, NE, C, L, Y, B, pi, mu, kappa): asset_mkt = A - B - labor_mkt = N_E - L + labor_mkt = NE - L goods_mkt = Y - C - mu/(mu-1)/(2*kappa) * (1+pi).apply(np.log)**2 * Y return asset_mkt, labor_mkt, goods_mkt @simple -def nkpc(pi, w, Z, Y, r, mu, kappa): - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1))\ - - (1 + pi).apply(np.log) - return nkpc_res - - -#TODO: this works but seems wrong conceptually -@simple -def partial_ss_solution(B_Y, Y, mu, r, kappa, Z, pi): - B = B_Y - w = 1 / mu - Div = (1 - w) - Tax = r * B - - nkpc_res = kappa * (w / Z - 1 / mu) + Y(+1) / Y * (1 + pi(+1)).apply(np.log) / (1 + r(+1)) - (1 + pi).apply(np.log) - - return B, w, Div, Tax, nkpc_res +def partial_ss_solution(B_Y, Y, Z, mu): + '''Solve (w) to hit targets for (nkpc_res)''' + w = Z / mu + B = B_Y * Y + return w, B '''Part 2: Embed HA block''' @@ -68,20 +64,30 @@ def make_grids(rho_s, sigma_s, nS, amax, nA): def transfers(pi_e, Div, Tax, e_grid): - # default incidence rules are proportional to skill; scale does not matter + # hardwired incidence rules are proportional to skill; scale does not matter tax_rule, div_rule = e_grid, e_grid - div = Div / np.sum(pi_e * div_rule) * div_rule tax = Tax / np.sum(pi_e * tax_rule) * tax_rule T = div - tax return T +def wages(w, e_grid): + we = w * e_grid + return we + + +def labor_supply(n, e_grid): + ne = e_grid[:, np.newaxis] * n + return ne + + '''Part 3: DAG''' def dag(): # Combine blocks - household = hh.household.add_hetinputs([transfers]) + household = hh.household.add_hetinputs([transfers, wages]) + household = household.add_hetoutputs([labor_supply]) household = combine([make_grids, household], name='HH') blocks = [household, firm, monetary, fiscal, mkt_clearing, nkpc] helper_blocks = [partial_ss_solution] @@ -90,17 +96,17 @@ def dag(): # Steady state calibration = {'r': 0.005, 'rstar': 0.005, 'eis': 0.5, 'frisch': 0.5, 'B_Y': 5.6, 'mu': 1.2, 'rho_s': 0.966, 'sigma_s': 0.5, 'kappa': 0.1, 'phi': 1.5, - 'Y': 1, 'Z': 1, 'L': 1, 'pi': 0, 'nS': 2, 'amax': 150, 'nA': 10} + 'Y': 1., 'Z': 1., 'pi': 0., 'nS': 2, 'amax': 150, 'nA': 10} unknowns_ss = {'beta': 0.986, 'vphi': 0.8, 'w': 0.8} - targets_ss = {'asset_mkt': 0, 'labor_mkt': 0, 'nkpc_res': 0.} + targets_ss = {'asset_mkt': 0., 'NE': 1., 'nkpc_res': 0.} ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='broyden_custom', helper_blocks=helper_blocks, helper_targets=['nkpc_res']) # Transitional dynamics - unknowns = ['pi', 'w', 'Y'] - targets = ['nkpc_res', 'asset_mkt', 'labor_mkt'] + unknowns = ['w', 'Y', 'pi'] + targets = ['asset_mkt', 'goods_mkt', 'nkpc_res'] exogenous = ['rstar', 'Z'] return hank_model, ss, unknowns, targets, exogenous diff --git a/src/sequence_jacobian/examples/hetblocks/household_labor.py b/src/sequence_jacobian/examples/hetblocks/household_labor.py index ad17dba..9278b6b 100644 --- a/src/sequence_jacobian/examples/hetblocks/household_labor.py +++ b/src/sequence_jacobian/examples/hetblocks/household_labor.py @@ -15,33 +15,28 @@ def household_init(a_grid, e_grid, r, w, eis, T): @het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, a_grid, e_grid, T, w, r, beta, eis, frisch, vphi): +def household(Va_p, a_grid, we, T, r, beta, eis, frisch, vphi): '''Single backward step via EGM.''' - # TODO: ws shoukld be hetinput - ws = w * e_grid uc_nextgrid = beta * Va_p - c_nextgrid, n_nextgrid = cn(uc_nextgrid, ws[:, np.newaxis], eis, frisch, vphi) + c_nextgrid, n_nextgrid = cn(uc_nextgrid, we[:, np.newaxis], eis, frisch, vphi) - lhs = c_nextgrid - ws[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] + lhs = c_nextgrid - we[:, np.newaxis] * n_nextgrid + a_grid[np.newaxis, :] - T[:, np.newaxis] rhs = (1 + r) * a_grid c = utils.interpolate.interpolate_y(lhs, rhs, c_nextgrid) n = utils.interpolate.interpolate_y(lhs, rhs, n_nextgrid) - a = rhs + ws[:, np.newaxis] * n + T[:, np.newaxis] - c + a = rhs + we[:, np.newaxis] * n + T[:, np.newaxis] - c iconst = np.nonzero(a < a_grid[0]) a[iconst] = a_grid[0] if iconst[0].size != 0 and iconst[1].size != 0: - c[iconst], n[iconst] = solve_cn(ws[iconst[0]], + c[iconst], n[iconst] = solve_cn(we[iconst[0]], rhs[iconst[1]] + T[iconst[0]] - a_grid[0], eis, frisch, vphi, Va_p[iconst]) Va = (1 + r) * c ** (-1 / eis) - # TODO: make hetoutput - n_e = e_grid[:, np.newaxis] * n - - return Va, a, c, n, n_e + return Va, a, c, n '''Supporting functions for HA block''' diff --git a/src/sequence_jacobian/examples/hetblocks/household_twoasset.py b/src/sequence_jacobian/examples/hetblocks/household_twoasset.py index 6f18f1d..220b56b 100644 --- a/src/sequence_jacobian/examples/hetblocks/household_twoasset.py +++ b/src/sequence_jacobian/examples/hetblocks/household_twoasset.py @@ -96,16 +96,13 @@ def household(Va_p, Vb_p, a_grid, b_grid, z_grid, e_grid, k_grid, beta, eis, rb, # solve out budget constraint to get consumption and marginal utility c = addouter(z_grid, (1 + rb) * b_grid, (1 + ra) * a_grid) - Psi - a - b uc = c ** (-1 / eis) - - # TODO: maken into hetoutpout - # for GE wage Phillips curve we'll need endowment-weighted utility too - u = e_grid[:, np.newaxis, np.newaxis] * uc + uce = e_grid[:, np.newaxis, np.newaxis] * uc # update derivatives of value function using envelope conditions Va = (1 + ra - Psi2) * uc Vb = (1 + rb) * uc - return Va, Vb, a, b, c, u + return Va, Vb, a, b, c, uce '''Supporting functions for HA block''' diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py index 66bcacb..5ef9885 100644 --- a/src/sequence_jacobian/examples/krusell_smith.py +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -1,6 +1,3 @@ -import scipy.optimize as opt - -from .. import utilities as utils from ..blocks.simple_block import simple from ..blocks.combined_block import create_model from .hetblocks.household_sim import household @@ -25,11 +22,11 @@ def mkt_clearing(K, A, Y, C, delta): @simple def firm_ss_solution(r, Y, L, delta, alpha): + '''Solve for (Z, K) given targets for (Y, r).''' rk = r + delta - w = (1 - alpha) * Y / L K = alpha * Y / rk Z = Y / K ** alpha / L ** (1 - alpha) - return w, K, Z + return K, Z '''Part 2: DAG''' diff --git a/src/sequence_jacobian/examples/two_asset.py b/src/sequence_jacobian/examples/two_asset.py index e046b31..3c44a7b 100644 --- a/src/sequence_jacobian/examples/two_asset.py +++ b/src/sequence_jacobian/examples/two_asset.py @@ -74,8 +74,8 @@ def wage(pi, w): @simple -def union(piw, N, tax, w, U, kappaw, muw, vphi, frisch, beta): - wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * U / muw) + beta * \ +def union(piw, N, tax, w, UCE, kappaw, muw, vphi, frisch, beta): + wnkpc = kappaw * (vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * N * UCE / muw) + beta * \ (1 + piw(+1)).apply(np.log) - (1 + piw).apply(np.log) return wnkpc @@ -130,10 +130,10 @@ def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): @simple -def partial_ss_step2(tax, w, U, N, muw, frisch): +def partial_ss_step2(tax, w, UCE, N, muw, frisch): """Solves for (vphi) to hit (wnkpc).""" - vphi = (1 - tax) * w * U / muw / N ** (1 + 1 / frisch) - wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * U / muw + vphi = (1 - tax) * w * UCE / muw / N ** (1 + 1 / frisch) + wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * UCE / muw return vphi, wnkpc diff --git a/tests/base/test_two_asset.py b/tests/base/test_two_asset.py index 8aba5f2..0c512d8 100644 --- a/tests/base/test_two_asset.py +++ b/tests/base/test_two_asset.py @@ -7,10 +7,10 @@ def test_hank_ss(): - A, B, U = hank_ss_singlerun() + A, B, UCE = hank_ss_singlerun() assert np.isclose(A, 12.526539492650361) assert np.isclose(B, 1.0840860793350566) - assert np.isclose(U, 4.5102870939550055) + assert np.isclose(UCE, 4.5102870939550055) def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg=2.8, G=0.2, @@ -43,7 +43,7 @@ def hank_ss_singlerun(beta=0.976, r=0.0125, tot_wealth=14, K=10, delta=0.02, Bg= out = hh.household.steady_state(calibration) - return out['A'], out['B'], out['U'] + return out['A'], out['B'], out['UCE'] def test_Psi(): From 6f20cda23e5bae1f2697bbe69cccac392512fa7e Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Thu, 16 Sep 2021 15:51:43 -0400 Subject: [PATCH 273/288] Let solve_impulse methods return the shocks as well as all unknowns. More demanding est_workflow revealed problems with helper blocks and td_nonlin.internals. --- src/sequence_jacobian/blocks/block.py | 4 +- .../blocks/combined_block.py | 1 - .../examples/hetblocks/household_labor.py | 4 +- .../examples/hetblocks/household_sim.py | 21 +- .../examples/krusell_smith.py | 24 +- tests/base/test_workflow.py | 224 ++++++++++-------- 6 files changed, 151 insertions(+), 127 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 3352858..7e0a37e 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -216,7 +216,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ else: raise ValueError(f'No convergence after {opts["maxit"]} backward iterations!') - return results | U + return results | U | inputs solve_impulse_linear_options = {} @@ -237,7 +237,7 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).pack() dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) - return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) + return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) | dU | inputs solve_jacobian_options = {} diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 3ba41f0..a0e2e9b 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -86,7 +86,6 @@ def _impulse_linear(self, ss, inputs, outputs, Js, options): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() - #irf_lin_partial_eq = inputs.copy() irf_lin_partial_eq = deepcopy(inputs) for block in self.blocks: input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} diff --git a/src/sequence_jacobian/examples/hetblocks/household_labor.py b/src/sequence_jacobian/examples/hetblocks/household_labor.py index 9278b6b..571828f 100644 --- a/src/sequence_jacobian/examples/hetblocks/household_labor.py +++ b/src/sequence_jacobian/examples/hetblocks/household_labor.py @@ -7,9 +7,9 @@ from ... import utilities as utils -def household_init(a_grid, e_grid, r, w, eis, T): +def household_init(a_grid, we, r, eis, T): fininc = (1 + r) * a_grid + T[:, np.newaxis] - a_grid[0] - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + T[:, np.newaxis] + coh = (1 + r) * a_grid[np.newaxis, :] + we[:, np.newaxis] + T[:, np.newaxis] Va = (1 + r) * (0.1 * coh) ** (-1 / eis) return fininc, Va diff --git a/src/sequence_jacobian/examples/hetblocks/household_sim.py b/src/sequence_jacobian/examples/hetblocks/household_sim.py index 2ed4547..5529340 100644 --- a/src/sequence_jacobian/examples/hetblocks/household_sim.py +++ b/src/sequence_jacobian/examples/hetblocks/household_sim.py @@ -6,28 +6,17 @@ from ... import utilities as utils -def household_init(a_grid, e_grid, r, w, eis): - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] +def household_init(a_grid, y, r, eis): + coh = (1 + r) * a_grid[np.newaxis, :] + y[:, np.newaxis] Va = (1 + r) * (0.1 * coh) ** (-1 / eis) return Va -def income_state_vars(rho, sigma, nS): - e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) - return e_grid, Pi - - -def asset_state_vars(amax, nA): - a_grid = utils.discretize.agrid(amax=amax, n=nA) - return a_grid - - -@het(exogenous='Pi', policy='a', backward='Va', - hetinputs=[income_state_vars, asset_state_vars], backward_init=household_init) -def household(Va_p, a_grid, e_grid, r, w, beta, eis): +@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) +def household(Va_p, a_grid, y, r, beta, eis): uc_nextgrid = beta * Va_p c_nextgrid = uc_nextgrid ** (-eis) - coh = (1 + r) * a_grid[np.newaxis, :] + w * e_grid[:, np.newaxis] + coh = (1 + r) * a_grid[np.newaxis, :] + y[:, np.newaxis] a = utils.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) utils.optimized_routines.setmin(a, a_grid[0]) c = coh - a diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py index 5ef9885..acf06e0 100644 --- a/src/sequence_jacobian/examples/krusell_smith.py +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -1,6 +1,7 @@ +from .. import utilities as utils from ..blocks.simple_block import simple -from ..blocks.combined_block import create_model -from .hetblocks.household_sim import household +from ..blocks.combined_block import create_model, combine +from .hetblocks import household_sim as hh '''Part 1: Blocks''' @@ -29,11 +30,26 @@ def firm_ss_solution(r, Y, L, delta, alpha): return K, Z -'''Part 2: DAG''' +'''Part 2: Embed HA block''' + +@simple +def make_grids(rho, sigma, nS, amax, nA): + e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) + a_grid = utils.discretize.agrid(amax=amax, n=nA) + return e_grid, Pi, a_grid + + +def income(w, e_grid): + y = w * e_grid + return y + + +'''Part 3: DAG''' def dag(): # Combine blocks - blocks = [household, firm, mkt_clearing] + household = hh.household.add_hetinputs([income]) + blocks = [household, firm, make_grids, mkt_clearing] helper_blocks = [firm_ss_solution] ks_model = create_model(blocks, name="Krusell-Smith") diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 57a3c61..871a0eb 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -1,155 +1,175 @@ import numpy as np import sequence_jacobian as sj -from sequence_jacobian import het, simple, solved, combine, create_model +from sequence_jacobian import simple, solved, combine, create_model, markov_rouwenhorst, agrid from sequence_jacobian.classes.impulse_dict import ImpulseDict - -'''Part 1: Household block''' +from sequence_jacobian.examples.hetblocks import household_sim as hh -def household_init(a_grid, y, rpost, sigma): - c = np.maximum(1e-8, y[:, np.newaxis] + np.maximum(rpost, 0.04) * a_grid[np.newaxis, :]) - Va = (1 + rpost) * (c ** (-sigma)) - return Va +'''Part 1: Household block''' +@simple +def make_grids(rho_e, sd_e, nE, amin, amax, nA): + e_grid, e_dist, Pi = markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) + a_grid = agrid(amin=amin, amax=amax, n=nA) + return e_grid, e_dist, Pi, a_grid -@het(exogenous='Pi', policy='a', backward='Va', backward_init=household_init) -def household(Va_p, a_grid, y, rpost, beta, sigma): - c_nextgrid = (beta * Va_p) ** (-1 / sigma) - coh = (1 + rpost) * a_grid[np.newaxis, :] + y[:, np.newaxis] - a = sj.utilities.interpolate.interpolate_y(c_nextgrid + a_grid, coh, a_grid) # (x, xq, y) - sj.utilities.optimized_routines.setmin(a, a_grid[0]) - c = coh - a - uc = c ** (-sigma) - Va = (1 + rpost) * uc - return Va, a, c +def income(atw, N, e_grid, transfer): + y = atw * N * e_grid + transfer + return y -def get_mpcs(c, a, a_grid, rpost): - """Approximate mpc, with symmetric differences where possible, exactly setting mpc=1 for constrained agents.""" +def get_mpcs(c, a, a_grid, r): mpcs_ = np.empty_like(c) - post_return = (1 + rpost) * a_grid - - # symmetric differences away from boundaries - mpcs_[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / \ - (post_return[2:] - post_return[:-2]) - - # asymmetric first differences at boundaries + post_return = (1 + r) * a_grid + mpcs_[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (post_return[2:] - post_return[:-2]) mpcs_[:, 0] = (c[:, 1] - c[:, 0]) / (post_return[1] - post_return[0]) mpcs_[:, -1] = (c[:, -1] - c[:, -2]) / (post_return[-1] - post_return[-2]) - - # special case of constrained mpcs_[a == a_grid[0]] = 1 - return mpcs_ -def income(tau, Y, e_grid, e_dist, Gamma, transfer): - """Labor income on the grid.""" - gamma = e_grid ** (Gamma * np.log(Y)) / np.vdot(e_dist, e_grid ** (1 + Gamma * np.log(Y))) - y = (1 - tau) * Y * gamma * e_grid + transfer - return y - -@simple -def income_state_vars(rho_e, sd_e, nE): - e_grid, e_dist, Pi = sj.utilities.discretize.markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) - return e_grid, e_dist, Pi - - -def asset_state_vars(amin, amax, nA): - a_grid = sj.utilities.discretize.agrid(amin=amin, amax=amax, n=nA) - return a_grid - - -def mpcs(c, a, a_grid, rpost): - """MPC out of lump-sum transfer.""" - mpc = get_mpcs(c, a, a_grid, rpost) +def mpcs(c, a, a_grid, r): + mpc = get_mpcs(c, a, a_grid, r) return mpc -household = household.add_hetinputs([income, asset_state_vars]) -household = household.add_hetoutputs([mpcs]) +def weighted_uc(c, e_grid, eis): + uce = c ** (-1 / eis) * e_grid[:, np.newaxis] + return uce '''Part 2: rest of the model''' +@solved(unknowns={'C': 1.0, 'A': 1.0}, targets=['euler', 'budget_constraint'], solver='broyden_custom') +def household_ra(C, A, r, atw, N, transfer, beta, eis): + euler = beta * (1 + r(1)) * C(1) ** (-1 / eis) - C ** (-1 / eis) + budget_constraint = (1 + r) * A(-1) + atw * N + transfer - C - A + UCE = C ** (-1 / eis) + return euler, budget_constraint, UCE -@simple -def interest_rates(r): - rpost = r(-1) # household ex-post return - rb = r(-1) # rate on 1-period real bonds - return rpost, rb +@simple +def firm(N, Z): + Y = Z * N + w = Z + return Y, w -# @simple -# def fiscal(B, G, rb, Y, transfer): -# rev = rb * B + G + transfer # revenue to be raised -# tau = rev / Y -# return rev, tau @simple -def fiscal(B, G, rb, Y, transfer, rho_B): - B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau +def union(UCE, tau, w, N, pi, muw, kappaw, nu, vphi, beta): + wnkpc = kappaw * N * (vphi * N ** nu - (1 - tau) * w * UCE / muw) + \ + beta * (1 + pi(+1)).apply(np.log) - (1 + pi).apply(np.log) + return wnkpc @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal_solved(B, G, rb, Y, transfer, rho_B): +def fiscal(B, G, rb, w, N, transfer, rho_B): B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised - tau = rev / Y - return B_rule, rev, tau + rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + tau = rev / (w * N) + atw = (1 - tau) * w + return B_rule, rev, tau, atw + + +# Use this to test zero impulse once we have it +@simple +def real_bonds(r): + rb = r(-1) + return rb @simple -def mkt_clearing(A, B, C, Y, G): +def mkt_clearing(A, B, C, G, Y): asset_mkt = A - B - goods_mkt = Y - C - G + goods_mkt = C + G - Y return asset_mkt, goods_mkt +'''Part 3: Helper blocks''' -'''Tests''' +@simple +def household_ra_ss(r, B, tau, w, N, transfer, eis): + beta = 1 / (1 + r) + A = B + C = r * A + (1 - tau) * w * N + transfer + UCE = C ** (-1 / eis) + return beta, A, C, UCE -def test_all(): - hh = combine([household, income_state_vars], name='HH') - calibration = {'Y': 1.0, 'r': 0.005, 'sigma': 2.0, 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, - 'amin': 0.0, 'amax': 1000, 'nA': 100, 'Gamma': 0.0, 'transfer': 0.143, 'rho_B': 0.8} - - # DAG with SimpleBlock `fiscal` - dag0 = create_model([hh, interest_rates, fiscal, mkt_clearing], name='HANK') - ss0 = dag0.solve_steady_state(calibration, solver='hybr', - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'MPC': 0.25}) +@simple +def union_ss(atw, UCE, muw, N, nu, kappaw, beta, pi): + vphi = atw * UCE / (muw * N ** nu) + wnkpc = kappaw * N * (vphi * N ** nu - atw * UCE / muw) + \ + beta * (1 + pi(+1)).apply(np.log) - (1 + pi).apply(np.log) + return wnkpc, vphi - # DAG with SolvedBlock `fiscal_solved` - dag1 = create_model([hh, interest_rates, fiscal_solved, mkt_clearing], name='HANK') - ss1 = dag1.solve_steady_state(calibration, solver='hybr', dissolve=['fiscal_solved'], - unknowns={'beta': .95, 'G': 0.2, 'B': 2.0}, - targets={'asset_mkt': 0.0, 'tau': 0.334, 'MPC': 0.25}) - assert all(np.allclose(ss0[k], ss1[k]) for k in ss0) - # Precompute household Jacobian - Js = {'household': household.jacobian(ss1, inputs=['Y', 'rpost', 'tau', 'transfer'], outputs=['C', 'A'], T=300)} +'''Tests''' + +def test_all(): + # Assemble HA block (want to test nesting) + household_ha = hh.household.add_hetinputs([income]) + household_ha = household_ha.add_hetoutputs([mpcs, weighted_uc]).rename('household_ha') + household_ha = combine([household_ha, make_grids], name='HH') + + # Assemble DAG (for transition dynamics) + dag = {} + common_blocks = [firm, union, fiscal, real_bonds, mkt_clearing] + dag['ha'] = create_model([household_ha] + common_blocks, name='HANK') + dag['ra'] = create_model([household_ra] + common_blocks, name='RANK') + unknowns = ['N', 'pi'] + targets = ['asset_mkt', 'wnkpc'] + + # Solve steady state + calibration = {'N': 1.0, 'Z': 1.0, 'r': 0.005, 'pi': 0.0, 'eis': 0.5, 'nu': 0.5, + 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, 'amin': 0.0, 'amax': 200, + 'nA': 100, 'kappaw': 0.1, 'muw': 1.2, 'transfer': 0.143, 'rho_B': 0.9} + + ss = {} + ss['ha'] = dag['ha'].solve_steady_state(calibration, dissolve=['fiscal'], solver='hybr', + unknowns={'beta': 0.96, 'B': 3.0, 'vphi': 1.0, 'G': 0.2}, + targets={'asset_mkt': 0.0, 'MPC': 0.25, 'wnkpc': 0.0, 'tau': 0.334}, + helper_blocks=[union_ss], helper_targets=['wnkpc']) + + # TODO: why doesn't this work? + # ss['ra'] = dag['ra'].solve_steady_state(ss['ha'], + # unknowns={'beta': 0.96, 'vphi': 1.0}, targets={'asset_mkt': 0.0, 'wnkpc': 0.0}, + # helper_blocks=[household_ra_ss, union_ss], helper_targets=['asset_mkt', 'wnkpc'], + # dissolve=['fiscal', 'household_ra'], solver='solved') + + # Constructing ss-dag manually works just fine + dag_ss = create_model([household_ra_ss, union_ss, firm, fiscal, + real_bonds, mkt_clearing]) + ss['ra'] = dag_ss.steady_state(ss['ha'], dissolve=['fiscal']) + assert np.isclose(ss['ra']['goods_mkt'], 0.0) + assert np.isclose(ss['ra']['asset_mkt'], 0.0) + assert np.isclose(ss['ra']['wnkpc'], 0.0) + + # Precompute HA Jacobian + Js = {'ra': {}, 'ha': {}} + Js['ha']['household_ha'] = household_ha.jacobian(ss['ha'], + inputs=['N', 'atw', 'r', 'transfer'], outputs=['C', 'A', 'UCE'], T=300) # Linear impulse responses from Jacobian vs directly - G = dag1.solve_jacobian(ss1, inputs=['r'], outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], - unknowns=['Y'], targets=['asset_mkt'], T=300, Js=Js) - shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) - td_lin1 = G @ shock - td_lin2 = dag1.solve_impulse_linear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) - assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) + shock = ImpulseDict({'G': 1E-4 * 0.9 ** np.arange(300)}) + G, td_lin1, td_lin2 = dict(), dict(), dict() + for k in ['ra', 'ha']: + G[k] = dag[k].solve_jacobian(ss[k], unknowns, targets, inputs=['G'], T=300, Js=Js[k], + outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) + td_lin1[k] = G[k] @ shock + td_lin2[k] = dag[k].solve_impulse_linear(ss[k], unknowns, targets, shock, Js=Js[k], + outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) + + assert all(np.allclose(td_lin1[k][i], td_lin2[k][i]) for i in td_lin1[k]) # Nonlinear vs linear impulses - td_nonlin = dag1.solve_impulse_nonlinear(ss1, unknowns=['Y'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js, internals=['household']) + td_nonlin = dag['ha'].solve_impulse_nonlinear(ss['ha'], unknowns, targets, + inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], + Js=Js, internals=['household_ha']) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') + assert all(np.allclose(td_lin1['ha'][k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1['ha'] if k != 'MPC') # See if D change matches up with aggregate assets - assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['a'], axis=(1,2)), td_nonlin['A']) + assert np.allclose(np.sum(td_nonlin.internals['household_ha']['D']*td_nonlin.internals['household_ha']['c'], axis=(1, 2)), td_nonlin['C']) From c63d4e1e08668ea40a92abb140c2836b6a6172cd Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Fri, 17 Sep 2021 13:31:27 -0400 Subject: [PATCH 274/288] fixed small timing issue in test_workflow --- src/sequence_jacobian/examples/krusell_smith.py | 1 + tests/base/test_workflow.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py index acf06e0..78b91fa 100644 --- a/src/sequence_jacobian/examples/krusell_smith.py +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -80,6 +80,7 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): def remapped_dag(): # Create 2 versions of the household block using `remap` + household = hh.household.add_hetinputs([income]) to_map = ['beta', *household.outputs] hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 871a0eb..4f81929 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -1,5 +1,4 @@ import numpy as np -import sequence_jacobian as sj from sequence_jacobian import simple, solved, combine, create_model, markov_rouwenhorst, agrid from sequence_jacobian.classes.impulse_dict import ImpulseDict from sequence_jacobian.examples.hetblocks import household_sim as hh @@ -75,7 +74,7 @@ def fiscal(B, G, rb, w, N, transfer, rho_B): # Use this to test zero impulse once we have it @simple def real_bonds(r): - rb = r(-1) + rb = r return rb @@ -105,7 +104,6 @@ def union_ss(atw, UCE, muw, N, nu, kappaw, beta, pi): return wnkpc, vphi - '''Tests''' def test_all(): From 9dc95a3f7e4a07a1f5a69525ced0a1742226ae9b Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 17 Sep 2021 14:29:46 -0500 Subject: [PATCH 275/288] eliminated ZeroMatrix, enforcing convention for ImpulseDicts that missing entries correspond to zero impulses (can use .get as alternative to .__getitem__ to fill in zeros), enforcing similar convention for JacobianDict inputs --- src/sequence_jacobian/blocks/block.py | 2 +- src/sequence_jacobian/blocks/simple_block.py | 14 +++--- src/sequence_jacobian/classes/__init__.py | 2 +- src/sequence_jacobian/classes/impulse_dict.py | 12 +++++ .../classes/jacobian_dict.py | 48 +++++++++++------- .../classes/sparse_jacobians.py | 49 ------------------- tests/base/test_jacobian_dict_block.py | 2 +- 7 files changed, 50 insertions(+), 79 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 7e0a37e..06febf4 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -234,7 +234,7 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) - dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).pack() + dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).get(targets).pack() # .get(targets) fills in zeros dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) | dU | inputs diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index bed741a..1d38543 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -5,7 +5,7 @@ from .support.simple_displacement import ignore, Displace, AccumulatedDerivative from .block import Block -from ..classes import SteadyStateDict, ImpulseDict, JacobianDict, SimpleSparse, ZeroMatrix +from ..classes import SteadyStateDict, ImpulseDict, JacobianDict, SimpleSparse from ..utilities import misc from ..utilities.function import ExtendedFunction @@ -71,16 +71,14 @@ def _jacobian(self, ss, inputs, outputs, T): # Because we computed the Jacobian of all outputs with respect to each shock (invertedJ[i][o]), # we need to loop back through to have J[o][i] to map for a given output `o`, shock `i`, # the Jacobian curlyJ^{o,i}. - J = {o: {} for o in self.outputs} - for o in self.outputs: + J = {o: {} for o in outputs} + for o in outputs: for i in inputs: - # Keep zeros, so we can inspect supplied Jacobians for completeness - if not invertedJ[i][o] or invertedJ[i][o].iszero: - J[o][i] = ZeroMatrix() - else: + # drop zeros from JacobianDict + if invertedJ[i][o] and not invertedJ[i][o].iszero: J[o][i] = invertedJ[i][o] - return JacobianDict(J, name=self.name, T=T)[outputs, :] + return JacobianDict(J, outputs, inputs, self.name, T) def compute_single_shock_J(self, ss, i): input_args = {i: ignore(ss[i]) for i in self.inputs} diff --git a/src/sequence_jacobian/classes/__init__.py b/src/sequence_jacobian/classes/__init__.py index 651bfa0..44a63e8 100644 --- a/src/sequence_jacobian/classes/__init__.py +++ b/src/sequence_jacobian/classes/__init__.py @@ -1,4 +1,4 @@ from .steady_state_dict import SteadyStateDict, UserProvidedSS from .impulse_dict import ImpulseDict from .jacobian_dict import JacobianDict, FactoredJacobianDict -from .sparse_jacobians import ZeroMatrix, IdentityMatrix, SimpleSparse +from .sparse_jacobians import IdentityMatrix, SimpleSparse diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index d656373..3389b74 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -98,3 +98,15 @@ def infer_length(self): if length != min(lengths): raise ValueError(f'Building ImpulseDict with inconsistent lengths {max(lengths)} and {min(lengths)}') return length + + def get(self, k): + """Like __getitem__ but with default of zero impulse""" + if isinstance(k, str): + return self.toplevel.get(k, np.zeros(self.T)) + elif isinstance(k, tuple): + raise TypeError(f'Key {k} to {type(self).__name__} cannot be tuple') + else: + try: + return type(self)({ki: self.toplevel.get(ki, np.zeros(self.T)) for ki in k}) + except TypeError: + raise TypeError(f'Key {k} to {type(self).__name__} needs to be a string or an iterable (list, set, etc) of strings') diff --git a/src/sequence_jacobian/classes/jacobian_dict.py b/src/sequence_jacobian/classes/jacobian_dict.py index 548079a..aeda2e4 100644 --- a/src/sequence_jacobian/classes/jacobian_dict.py +++ b/src/sequence_jacobian/classes/jacobian_dict.py @@ -6,12 +6,12 @@ from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection from .impulse_dict import ImpulseDict -from .sparse_jacobians import ZeroMatrix, IdentityMatrix, SimpleSparse, make_matrix +from .sparse_jacobians import IdentityMatrix, SimpleSparse, make_matrix from typing import Any, Dict, Union Array = Any -Jacobian = Union[np.ndarray, ZeroMatrix, IdentityMatrix, SimpleSparse] +Jacobian = Union[np.ndarray, IdentityMatrix, SimpleSparse] class NestedDict: def __init__(self, nesteddict, outputs: OrderedSet=None, inputs: OrderedSet=None, name: str=None): @@ -68,11 +68,11 @@ def __getitem__(self, x): return self.nesteddict[o][i] else: # case 2b: one output, multiple inputs, return dict - return {ii: self.nesteddict[o][ii] for ii in i} + return subdict(self.nesteddict[o], i) else: # case 2c: multiple outputs, one or more inputs, return NestedDict with outputs o and inputs i i = (i,) if isinstance(i, str) else i - return type(self)({oo: {ii: self.nesteddict[oo][ii] for ii in i} for oo in o}, o, i) + return type(self)({oo: subdict(self.nesteddict[oo], i) for oo in o}, o, i) elif isinstance(x, OrderedSet) or isinstance(x, list) or isinstance(x, set): # case 3: assume that list or set refers just to outputs, get all of those return type(self)({oo: self.nesteddict[oo] for oo in x}, x, self.inputs) @@ -112,6 +112,11 @@ def deduplicate(mylist): return list(dict.fromkeys(mylist)) +def subdict(d, ks): + """Return subdict of d with only keys in ks (if some ks are not in d, ignore them)""" + return {k: d[k] for k in ks if k in d} + + class JacobianDict(NestedDict): def __init__(self, nesteddict, outputs=None, inputs=None, name=None, T=None, check=False): if check: @@ -121,11 +126,7 @@ def __init__(self, nesteddict, outputs=None, inputs=None, name=None, T=None, che @staticmethod def identity(ks): - return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks).complete() - - def complete(self): - # TODO: think about when and whether we want to use this - return super().complete(ZeroMatrix()) + return JacobianDict({k: {k: IdentityMatrix()} for k in ks}, ks, ks) def addinputs(self): """Add any inputs that were not already in output list as outputs, with the identity""" @@ -164,19 +165,22 @@ def compose(self, J): m_list = tuple(set(self.inputs) & set(J.outputs)) i_list = J.inputs - J_om = self.complete().nesteddict - J_mi = J.complete().nesteddict + J_om = self.nesteddict + J_mi = J.nesteddict J_oi = {} for o in o_list: J_oi[o] = {} for i in i_list: - Jout = ZeroMatrix() + Jout = None for m in m_list: - J_om[o][m] - J_mi[m][i] - Jout += J_om[o][m] @ J_mi[m][i] - J_oi[o][i] = Jout + if m in J_om[o] and i in J_mi[m]: + if Jout is None: + Jout = J_om[o][m] @ J_mi[m][i] + else: + Jout += J_om[o][m] @ J_mi[m][i] + if Jout is not None: + J_oi[o][i] = Jout return JacobianDict(J_oi, o_list, i_list) @@ -185,13 +189,15 @@ def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): x = ImpulseDict(x) inputs = x.keys() & set(self.inputs) - J_oi = self.complete().nesteddict + J_oi = self.nesteddict y = {} for o in self.outputs: y[o] = np.zeros(x.T) + J_i = J_oi[o] for i in inputs: - y[o] += J_oi[o][i] @ x[i] + if i in J_i: + y[o] += J_i[i] @ x[i] return ImpulseDict(y, T=x.T) @@ -208,7 +214,11 @@ def pack(self, T=None): J = np.empty((len(self.outputs) * T, len(self.inputs) * T)) for iO, O in enumerate(self.outputs): for iI, I in enumerate(self.inputs): - J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(self[O, I], T) + J_OI = self[O].get(I) + if J_OI is not None: + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = make_matrix(J_OI, T) + else: + J[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] = 0 return J @staticmethod diff --git a/src/sequence_jacobian/classes/sparse_jacobians.py b/src/sequence_jacobian/classes/sparse_jacobians.py index 4cdeb55..b995a82 100644 --- a/src/sequence_jacobian/classes/sparse_jacobians.py +++ b/src/sequence_jacobian/classes/sparse_jacobians.py @@ -49,55 +49,6 @@ def __repr__(self): return 'IdentityMatrix' -class ZeroMatrix: - """Simple zero matrix class, cheaper than using actual np.zeros((T,T)) matrix, - use in common case where some outputs don't depend on inputs""" - __array_priority__ = 10_000 - - def sparse(self): - return SimpleSparse({(0, 0): 0}) - - def matrix(self, T): - return np.zeros((T,T)) - - def __matmul__(self, other): - if isinstance(other, np.ndarray) and other.ndim == 1: - return np.zeros_like(other) - else: - return self - - def __rmatmul__(self, other): - return self @ other - - def __mul__(self, a): - return self - - def __rmul__(self, a): - return self - - # copies seem inefficient here, try to live without them - def __add__(self, x): - return x - - def __radd__(self, x): - return x - - def __sub__(self, x): - return -x - - def __rsub__(self, x): - return x - - def __neg__(self): - return self - - def __pos__(self): - return self - - def __repr__(self): - return 'ZeroMatrix' - - class SimpleSparse: """Efficient representation of sparse linear operators, which are linear combinations of basis operators represented by pairs (i, m), where i is the index of diagonal on which there are 1s diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 773648c..62c6f08 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -17,7 +17,7 @@ def test_jacobian_dict_block_impulses(rbc_dag): J_block_Z = J_block.jacobian(SteadyStateDict({}), ["Z"]) for o in J_block_Z.outputs: - assert np.all(J_block[o]["Z"] == J_block_Z[o]["Z"]) + assert np.all(J_block[o].get("Z") == J_block_Z[o].get("Z")) dZ = 0.8 ** np.arange(T) From ed1a0a622c39b9f654d299a874b19a37f76fa09d Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Fri, 17 Sep 2021 16:08:10 -0500 Subject: [PATCH 276/288] uniformly included inputs as defaults in output for impulses and unknowns as defaults in output for solved methods (with an option to narrow down in both cases); have introduced some bug in test_transitional_dynamics.py solving for nonlinear two-asset transitions, need to find --- src/sequence_jacobian/blocks/block.py | 73 +++++++++++++------ .../blocks/combined_block.py | 10 +-- src/sequence_jacobian/classes/impulse_dict.py | 7 +- src/sequence_jacobian/classes/result_dict.py | 4 +- tests/base/test_simple_block.py | 4 +- 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 06febf4..0fd90cc 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -64,15 +64,17 @@ def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], around a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'impulse_nonlinear') inputs = ImpulseDict(inputs) - _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + actual_outputs, inputs_as_outputs = self.process_outputs(ss, self.make_ordered_set(inputs), self.make_ordered_set(outputs)) # SolvedBlocks may use Js and may be nested in a CombinedBlock if isinstance(self, Parent): - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, internals, Js, options, **own_options) + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, internals, Js, options, **own_options) elif hasattr(self, 'internals'): - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, self.internals_to_report(internals), **own_options) + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, self.internals_to_report(internals), **own_options) else: - return self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, **own_options) + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, **own_options) + + return inputs[inputs_as_outputs] | out def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: @@ -80,12 +82,14 @@ def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], Im around a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'impulse_linear') inputs = ImpulseDict(inputs) - _, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) + actual_outputs, inputs_as_outputs = self.process_outputs(ss, self.make_ordered_set(inputs), self.make_ordered_set(outputs)) if isinstance(self, Parent): - return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, options, **own_options) + out = self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, Js, options, **own_options) else: - return self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, Js, **own_options) + out = self.M @ self._impulse_linear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, Js, **own_options) + + return inputs[inputs_as_outputs] | out def partial_jacobians(self, ss: SteadyStateDict, inputs: Optional[List[str]] = None, outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs): @@ -115,8 +119,10 @@ def partial_jacobians(self, ss: SteadyStateDict, inputs: Optional[List[str]] = N def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" - inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) own_options = self.get_options(options, kwargs, 'jacobian') + inputs = self.make_ordered_set(inputs) + outputs, _ = self.process_outputs(ss, {}, self.make_ordered_set(outputs)) + #outputs = self.make_ordered_set(outputs) if outputs is not None else self.outputs # if you have a J for this block that has everything you need, use it if (self.name in Js) and isinstance(Js[self.name], JacobianDict) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): @@ -188,11 +194,14 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" inputs = ImpulseDict(inputs) - input_names, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + + input_names = self.make_ordered_set(inputs) + actual_outputs, inputs_as_outputs = self.process_outputs(ss, input_names | unknowns, self.make_ordered_set(outputs)) + T = inputs.T - Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) + Js = self.partial_jacobians(ss, input_names | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs) H_U_factored = FactoredJacobianDict(H_U, T) @@ -203,7 +212,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ if opts['verbose']: print(f'Solving {self.name} for {unknowns} to hit {targets}') for it in range(opts['maxit']): - results = self.impulse_nonlinear(ss, inputs | U, outputs | targets, internals, Js, options, **kwargs) + results = self.impulse_nonlinear(ss, inputs | U, actual_outputs | targets, internals, Js, options, **kwargs) errors = {k: np.max(np.abs(results[k])) for k in targets} if opts['verbose']: print(f'On iteration {it}') @@ -216,7 +225,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ else: raise ValueError(f'No convergence after {opts["maxit"]} backward iterations!') - return results | U | inputs + return (inputs | U)[inputs_as_outputs] | results solve_impulse_linear_options = {} @@ -227,34 +236,33 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" inputs = ImpulseDict(inputs) - input_names, outputs = self.default_inputs_outputs(ss, inputs.keys(), outputs) unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + + input_names = self.make_ordered_set(inputs) + actual_outputs, inputs_as_outputs = self.process_outputs(ss, input_names | unknowns, self.make_ordered_set(outputs)) + T = inputs.T - Js = self.partial_jacobians(ss, input_names | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) + Js = self.partial_jacobians(ss, input_names | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).get(targets).pack() # .get(targets) fills in zeros dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) - return self.impulse_linear(ss, dU | inputs, outputs, Js, options, **kwargs) | dU | inputs + return (inputs | dU)[inputs_as_outputs] | self.impulse_linear(ss, dU | inputs, actual_outputs, Js, options, **kwargs) solve_jacobian_options = {} def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], - inputs: List[str], outputs: Optional[List[str]] = None, T: Optional[int] = None, + inputs: List[str], outputs: Optional[List[str]] = None, T: int = 300, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" - # TODO: do we really want this? is T just optional because we want it to come after outputs in docstring? - if T is None: - T = 300 - - inputs, outputs = self.default_inputs_outputs(ss, inputs, outputs) - unknowns, targets = OrderedSet(unknowns), OrderedSet(targets) + inputs, unknowns = self.make_ordered_set(inputs), self.make_ordered_set(unknowns) + actual_outputs, unknowns_as_outputs = self.process_outputs(ss, unknowns, self.make_ordered_set(outputs)) - Js = self.partial_jacobians(ss, inputs | unknowns, (outputs | targets) - unknowns, T, Js, options, **kwargs) + Js = self.partial_jacobians(ss, inputs | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) H_Z = self.jacobian(ss, inputs, targets, T, Js, options, **kwargs).pack(T) @@ -262,7 +270,7 @@ def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List from sequence_jacobian import combine self_with_unknowns = combine([U_Z, self]) - return self_with_unknowns.jacobian(ss, inputs, unknowns | outputs, T, Js, options, **kwargs) + return self_with_unknowns.jacobian(ss, inputs, unknowns_as_outputs | actual_outputs, T, Js, options, **kwargs) def solved(self, unknowns, targets, name=None, solver=None, solver_kwargs=None): if name is None: @@ -297,6 +305,23 @@ def default_inputs_outputs(self, ss: SteadyStateDict, inputs, outputs): outputs = self.outputs - ss._vector_valued() return OrderedSet(inputs), OrderedSet(outputs) + def process_outputs(self, ss, inputs: OrderedSet, outputs: Optional[OrderedSet]): + if outputs is None: + actual_outputs = self.outputs - ss._vector_valued() + inputs_as_outputs = inputs + else: + actual_outputs = outputs & self.outputs + inputs_as_outputs = outputs & inputs + + return actual_outputs, inputs_as_outputs + + @staticmethod + def make_ordered_set(x): + if x is not None and not isinstance(x, OrderedSet): + return OrderedSet(x) + else: + return x + def get_options(self, options: dict, kwargs, method): own_options = getattr(self, method + "_options") diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index a0e2e9b..2903934 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -80,20 +80,20 @@ def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options): if input_args: # If this block is actually perturbed impulses.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, internals, Js, options)) - return ImpulseDict({k: impulses.toplevel[k] for k in original_outputs}, impulses.internals, impulses.T) + return ImpulseDict({k: impulses.toplevel[k] for k in original_outputs if k in impulses.toplevel}, impulses.internals, impulses.T) def _impulse_linear(self, ss, inputs, outputs, Js, options): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() - irf_lin_partial_eq = deepcopy(inputs) + impulses = inputs.copy() for block in self.blocks: - input_args = {k: v for k, v in irf_lin_partial_eq.items() if k in block.inputs} + input_args = {k: v for k, v in impulses.items() if k in block.inputs} if input_args: # If this block is actually perturbed - irf_lin_partial_eq.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js, options)) + impulses.update(block.impulse_linear(ss, input_args, outputs & block.outputs, Js, options)) - return irf_lin_partial_eq[original_outputs] + return ImpulseDict({k: impulses.toplevel[k] for k in original_outputs if k in impulses.toplevel}, T=impulses.T) def _partial_jacobians(self, ss, inputs, outputs, T, Js, options): vector_valued = ss._vector_valued() diff --git a/src/sequence_jacobian/classes/impulse_dict.py b/src/sequence_jacobian/classes/impulse_dict.py index 3389b74..b0b9b46 100644 --- a/src/sequence_jacobian/classes/impulse_dict.py +++ b/src/sequence_jacobian/classes/impulse_dict.py @@ -21,6 +21,9 @@ def __init__(self, data, internals=None, T=None): super().__init__(data, internals) self.T = (T if T is not None else self.infer_length()) + def __getitem__(self, k): + return super().__getitem__(k, T=self.T) + def __add__(self, other): return self.binary_operation(other, lambda a, b: a + b) @@ -90,7 +93,7 @@ def unpack(bigv, outputs, T): impulse = {} for i, o in enumerate(outputs): impulse[o] = bigv[i*T:(i+1)*T] - return ImpulseDict(impulse) + return ImpulseDict(impulse, T=T) def infer_length(self): lengths = [len(v) for v in self.toplevel.values()] @@ -107,6 +110,6 @@ def get(self, k): raise TypeError(f'Key {k} to {type(self).__name__} cannot be tuple') else: try: - return type(self)({ki: self.toplevel.get(ki, np.zeros(self.T)) for ki in k}) + return type(self)({ki: self.toplevel.get(ki, np.zeros(self.T)) for ki in k}, T=self.T) except TypeError: raise TypeError(f'Key {k} to {type(self).__name__} needs to be a string or an iterable (list, set, etc) of strings') diff --git a/src/sequence_jacobian/classes/result_dict.py b/src/sequence_jacobian/classes/result_dict.py index 834e880..5732182 100644 --- a/src/sequence_jacobian/classes/result_dict.py +++ b/src/sequence_jacobian/classes/result_dict.py @@ -22,14 +22,14 @@ def __repr__(self): def __iter__(self): return iter(self.toplevel) - def __getitem__(self, k): + def __getitem__(self, k, **kwargs): if isinstance(k, str): return self.toplevel[k] elif isinstance(k, tuple): raise TypeError(f'Key {k} to {type(self).__name__} cannot be tuple') else: try: - return type(self)({ki: self.toplevel[ki] for ki in k}) + return type(self)({ki: self.toplevel[ki] for ki in k}, **kwargs) except TypeError: raise TypeError(f'Key {k} to {type(self).__name__} needs to be a string or an iterable (list, set, etc) of strings') diff --git a/tests/base/test_simple_block.py b/tests/base/test_simple_block.py index d01329a..95155e7 100644 --- a/tests/base/test_simple_block.py +++ b/tests/base/test_simple_block.py @@ -57,8 +57,8 @@ def test_block_consistency(block, ss): td_up = block.impulse_nonlinear(ss_results, {i: h*shock for i, shock in all_shocks.items()}) td_dn = block.impulse_nonlinear(ss_results, {i: -h*shock for i, shock in all_shocks.items()}) - linear_impulses = {o: (td_up[o] - td_dn[o])/(2*h) for o in td_up} - linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in td_up} + linear_impulses = {o: (td_up[o] - td_dn[o])/(2*h) for o in block.outputs} + linear_impulses_from_jac = {o: sum(J[o][i] @ all_shocks[i] for i in all_shocks if i in J[o]) for o in block.outputs} for o in linear_impulses: assert np.all(np.abs(linear_impulses[o] - linear_impulses_from_jac[o]) < 1E-5) From ff95aa54dd9a81b15a6ed5364b8fd44432346d78 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 20 Sep 2021 15:17:24 -0400 Subject: [PATCH 277/288] Debugged transitional dynamics: solved_block did not return unknowns properly hence asset_mkt clearing was off. Verified that consistency of micro and macro responses was always spurious. Test passed before because assets were not responding in equilibrium. --- src/sequence_jacobian/blocks/solved_block.py | 4 +- tests/base/test_workflow.py | 76 +++++++++++++++----- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 6f8cd9b..f306856 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -71,11 +71,11 @@ def _steady_state(self, calibration, dissolve, options, **kwargs): def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, **kwargs): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs - self.unknowns.keys(), internals, Js, options, **kwargs) + inputs, outputs, internals, Js, options, **kwargs) def _impulse_linear(self, ss, inputs, outputs, Js, options): return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs - self.unknowns.keys(), Js, options) + inputs, outputs, Js, options) def _jacobian(self, ss, inputs, outputs, T, Js, options): return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 4f81929..429b1e6 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -63,19 +63,19 @@ def union(UCE, tau, w, N, pi, muw, kappaw, nu, vphi, beta): @solved(unknowns={'B': (0.0, 10.0)}, targets=['B_rule'], solver='brentq') -def fiscal(B, G, rb, w, N, transfer, rho_B): +def fiscal(B, G, r, w, N, transfer, rho_B): B_rule = B.ss + rho_B * (B(-1) - B.ss + G - G.ss) - B - rev = (1 + rb) * B(-1) + G + transfer - B # revenue to be raised + rev = (1 + r) * B(-1) + G + transfer - B # revenue to be raised tau = rev / (w * N) atw = (1 - tau) * w return B_rule, rev, tau, atw # Use this to test zero impulse once we have it -@simple -def real_bonds(r): - rb = r - return rb +# @simple +# def real_bonds(r): +# rb = r +# return rb @simple @@ -114,7 +114,7 @@ def test_all(): # Assemble DAG (for transition dynamics) dag = {} - common_blocks = [firm, union, fiscal, real_bonds, mkt_clearing] + common_blocks = [firm, union, fiscal, mkt_clearing] dag['ha'] = create_model([household_ha] + common_blocks, name='HANK') dag['ra'] = create_model([household_ra] + common_blocks, name='RANK') unknowns = ['N', 'pi'] @@ -138,8 +138,7 @@ def test_all(): # dissolve=['fiscal', 'household_ra'], solver='solved') # Constructing ss-dag manually works just fine - dag_ss = create_model([household_ra_ss, union_ss, firm, fiscal, - real_bonds, mkt_clearing]) + dag_ss = create_model([household_ra_ss, union_ss, firm, fiscal, mkt_clearing]) ss['ra'] = dag_ss.steady_state(ss['ha'], dissolve=['fiscal']) assert np.isclose(ss['ra']['goods_mkt'], 0.0) assert np.isclose(ss['ra']['asset_mkt'], 0.0) @@ -151,23 +150,64 @@ def test_all(): inputs=['N', 'atw', 'r', 'transfer'], outputs=['C', 'A', 'UCE'], T=300) # Linear impulse responses from Jacobian vs directly - shock = ImpulseDict({'G': 1E-4 * 0.9 ** np.arange(300)}) + shock = ImpulseDict({'G': 0.9 ** np.arange(300)}) G, td_lin1, td_lin2 = dict(), dict(), dict() for k in ['ra', 'ha']: - G[k] = dag[k].solve_jacobian(ss[k], unknowns, targets, inputs=['G'], T=300, Js=Js[k], - outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) + G[k] = dag[k].solve_jacobian(ss[k], unknowns, targets, inputs=['G'], T=300, Js=Js[k]) td_lin1[k] = G[k] @ shock - td_lin2[k] = dag[k].solve_impulse_linear(ss[k], unknowns, targets, shock, Js=Js[k], - outputs=['Y', 'C', 'asset_mkt', 'goods_mkt']) + td_lin2[k] = dag[k].solve_impulse_linear(ss[k], unknowns, targets, shock, Js=Js[k]) assert all(np.allclose(td_lin1[k][i], td_lin2[k][i]) for i in td_lin1[k]) # Nonlinear vs linear impulses - td_nonlin = dag['ha'].solve_impulse_nonlinear(ss['ha'], unknowns, targets, - inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], + td_nonlin = dag['ha'].solve_impulse_nonlinear(ss['ha'], unknowns, targets, inputs=shock*1E-4, Js=Js, internals=['household_ha']) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1['ha'][k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1['ha'] if k != 'MPC') + assert all(np.allclose(td_lin1['ha'][k]*1E-4, td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1['ha'] if k != 'MPC') # See if D change matches up with aggregate assets - assert np.allclose(np.sum(td_nonlin.internals['household_ha']['D']*td_nonlin.internals['household_ha']['c'], axis=(1, 2)), td_nonlin['C']) + assert np.allclose(np.sum(td_nonlin.internals['household_ha']['D']*td_nonlin.internals['household_ha']['a'], axis=(1, 2)), td_nonlin['A']) + + +def test_all_old(): + # Assemble HA block (want to test nesting) + household = hh.household.add_hetinputs([income]) + household = household.add_hetoutputs([mpcs]) + household = combine([household, make_grids], name='HH') + + # Assemble DAG (for transition dynamics) + dag = create_model([household, firm, fiscal, mkt_clearing], name='HANK') + unknowns = ['N'] + targets = ['asset_mkt'] + + # Solve steady state + calibration = {'N': 1.0, 'Z': 1.0, 'r': 0.005, 'eis': 0.5, 'nu': 0.5, + 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, 'amin': 0.0, 'amax': 200, + 'nA': 100, 'transfer': 0.143, 'rho_B': 0.0} + + ss = dag.solve_steady_state(calibration, dissolve=['fiscal'], solver='hybr', + unknowns={'beta': 0.96, 'B': 3.0, 'G': 0.2}, + targets={'asset_mkt': 0.0, 'MPC': 0.25, 'tau': 0.334}) + + # Precompute HA Jacobian + Js = {'household': household.jacobian(ss, inputs=['N', 'atw', 'r', 'transfer'], + outputs=['C', 'A'], T=300)} + + # Linear impulse responses from Jacobian vs directly + G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], + unknowns=['N'], targets=['asset_mkt'], T=300, Js=Js) + shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) + td_lin1 = G @ shock + td_lin2 = dag.solve_impulse_linear(ss, unknowns=['N'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) + assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) + + # Nonlinear vs linear impulses + td_nonlin = dag.solve_impulse_nonlinear(ss, unknowns=['N'], targets=['asset_mkt'], + inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js, internals=['household']) + assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 + assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') + + # See if D change matches up with aggregate assets + assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['a'], axis=(1,2)), td_nonlin['A']) + assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['c'], axis=(1,2)), td_nonlin['C']) \ No newline at end of file From e7294e81dc5d9d7ba994e7c5c5a2d30c151a50e0 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Tue, 21 Sep 2021 08:36:55 -0400 Subject: [PATCH 278/288] Test of nonlinear impulse of internals was wrong. We forgot to add back ss before integrating. All tests pass. --- tests/base/test_workflow.py | 52 +++---------------------------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 429b1e6..63ff13f 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -156,58 +156,14 @@ def test_all(): G[k] = dag[k].solve_jacobian(ss[k], unknowns, targets, inputs=['G'], T=300, Js=Js[k]) td_lin1[k] = G[k] @ shock td_lin2[k] = dag[k].solve_impulse_linear(ss[k], unknowns, targets, shock, Js=Js[k]) - assert all(np.allclose(td_lin1[k][i], td_lin2[k][i]) for i in td_lin1[k]) # Nonlinear vs linear impulses - td_nonlin = dag['ha'].solve_impulse_nonlinear(ss['ha'], unknowns, targets, inputs=shock*1E-4, + td_nonlin = dag['ha'].solve_impulse_nonlinear(ss['ha'], unknowns, targets, inputs=shock*1E-2, Js=Js, internals=['household_ha']) assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1['ha'][k]*1E-4, td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1['ha'] if k != 'MPC') # See if D change matches up with aggregate assets - assert np.allclose(np.sum(td_nonlin.internals['household_ha']['D']*td_nonlin.internals['household_ha']['a'], axis=(1, 2)), td_nonlin['A']) - - -def test_all_old(): - # Assemble HA block (want to test nesting) - household = hh.household.add_hetinputs([income]) - household = household.add_hetoutputs([mpcs]) - household = combine([household, make_grids], name='HH') - - # Assemble DAG (for transition dynamics) - dag = create_model([household, firm, fiscal, mkt_clearing], name='HANK') - unknowns = ['N'] - targets = ['asset_mkt'] - - # Solve steady state - calibration = {'N': 1.0, 'Z': 1.0, 'r': 0.005, 'eis': 0.5, 'nu': 0.5, - 'rho_e': 0.91, 'sd_e': 0.92, 'nE': 3, 'amin': 0.0, 'amax': 200, - 'nA': 100, 'transfer': 0.143, 'rho_B': 0.0} - - ss = dag.solve_steady_state(calibration, dissolve=['fiscal'], solver='hybr', - unknowns={'beta': 0.96, 'B': 3.0, 'G': 0.2}, - targets={'asset_mkt': 0.0, 'MPC': 0.25, 'tau': 0.334}) - - # Precompute HA Jacobian - Js = {'household': household.jacobian(ss, inputs=['N', 'atw', 'r', 'transfer'], - outputs=['C', 'A'], T=300)} - - # Linear impulse responses from Jacobian vs directly - G = dag.solve_jacobian(ss, inputs=['r'], outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], - unknowns=['N'], targets=['asset_mkt'], T=300, Js=Js) - shock = ImpulseDict({'r': 1E-4 * 0.9 ** np.arange(300)}) - td_lin1 = G @ shock - td_lin2 = dag.solve_impulse_linear(ss, unknowns=['N'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js) - assert all(np.allclose(td_lin1[k], td_lin2[k]) for k in td_lin1) - - # Nonlinear vs linear impulses - td_nonlin = dag.solve_impulse_nonlinear(ss, unknowns=['N'], targets=['asset_mkt'], - inputs=shock, outputs=['Y', 'C', 'A', 'MPC', 'asset_mkt', 'goods_mkt'], Js=Js, internals=['household']) - assert np.max(np.abs(td_nonlin['goods_mkt'])) < 1E-8 - assert all(np.allclose(td_lin1[k], td_nonlin[k], atol=1E-6, rtol=1E-6) for k in td_lin1 if k != 'MPC') - - # See if D change matches up with aggregate assets - assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['a'], axis=(1,2)), td_nonlin['A']) - assert np.allclose(np.sum(td_nonlin.internals['household']['D']*td_nonlin.internals['household']['c'], axis=(1,2)), td_nonlin['C']) \ No newline at end of file + td_nonlin_lvl = td_nonlin + ss['ha'] + td_A = np.sum(td_nonlin_lvl.internals['household_ha']['a'] * td_nonlin_lvl.internals['household_ha']['D'], axis=(1, 2)) + assert np.allclose(td_A - ss['ha']['A'], td_nonlin['A']) From 288a57b24c9d25c47587233961c1f1e900df881d Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 21 Sep 2021 14:48:37 -0500 Subject: [PATCH 279/288] added working options for two-sided differentiation (for het_block jacobians) and specifying monotonic rules (for het_block.nonlinear) --- src/sequence_jacobian/blocks/het_block.py | 27 ++++++------ .../blocks/support/het_support.py | 18 +++++--- src/sequence_jacobian/utilities/function.py | 44 ++++++++++++++----- 3 files changed, 59 insertions(+), 30 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 0c24b1e..5f101d6 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -118,7 +118,7 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, internals, monotonic=False): individual_paths, exog_path = self.backward_nonlinear(ss, inputs, toreturn) # run forward iteration to get path of distribution, add to individual_paths - self.forward_nonlinear(ss, individual_paths, exog_path) + self.forward_nonlinear(ss, individual_paths, exog_path, monotonic) # obtain aggregates of all outputs, made uppercase aggregates = {o: utils.optimized_routines.fast_aggregate( @@ -131,7 +131,7 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, internals, monotonic=False): def _impulse_linear(self, ss, inputs, outputs, Js): return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) - def _jacobian(self, ss, inputs, outputs, T, h=1E-4): + def _jacobian(self, ss, inputs, outputs, T, h=1E-4, twosided=False): ss = self.extract_ss_dict(ss) self.update_with_hetinputs(ss) outputs = self.M_outputs.inv @ outputs @@ -139,7 +139,7 @@ def _jacobian(self, ss, inputs, outputs, T, h=1E-4): # step 0: preliminary processing of steady state exog = self.make_exog_law_of_motion(ss) endog = self.make_endog_law_of_motion(ss) - differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h, exog) + differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs = self.jac_backward_prelim(ss, h, exog, twosided) law_of_motion = CombinedTransition([exog, endog]).forward_shockable(ss['Dbeg']) exog_by_output = {k: exog.expectation_shockable(ss[k]) for k in outputs | self.backward} @@ -266,7 +266,7 @@ def backward_nonlinear(self, ss, inputs, toreturn): return individual_paths, exog_path[::-1] - def forward_nonlinear(self, ss, individual_paths, exog_path): + def forward_nonlinear(self, ss, individual_paths, exog_path, monotonic): T = len(exog_path) Dbeg = ss['Dbeg'] @@ -275,7 +275,7 @@ def forward_nonlinear(self, ss, individual_paths, exog_path): D_path = np.empty_like(Dbeg_path) for t in range(T): - endog = self.make_endog_law_of_motion({**ss, **{k: individual_paths[k][t, ...] for k in self.policy}}) + endog = self.make_endog_law_of_motion({**ss, **{k: individual_paths[k][t, ...] for k in self.policy}}, monotonic) # now step forward in two, first exogenous this period then endogenous D_path[t, ...] = exog_path[t].forward(Dbeg) @@ -296,7 +296,7 @@ def backward_fakenews(self, input_shocked, output_list, T, differentiable_backwa # contemporaneous effect of unit scalar shock to input_shocked din_dict = {input_shocked: 1} if differentiable_hetinput is not None and input_shocked in differentiable_hetinput.inputs: - din_dict.update(differentiable_hetinput.diff2({input_shocked: 1})) + din_dict.update(differentiable_hetinput.diff({input_shocked: 1})) curlyV, curlyD, curlyY = self.backward_step_fakenews(din_dict, output_list, differentiable_backward_fun, differentiable_hetoutput, law_of_motion, exog, True) @@ -390,20 +390,21 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_backward_ return curlyV, curlyD, curlyY - def jac_backward_prelim(self, ss, h, exog): + def jac_backward_prelim(self, ss, h, exog, twosided): """Support for part 1 of fake news algorithm: preload differentiable functions""" differentiable_hetinputs = None if self.hetinputs is not None: - differentiable_hetinputs = self.hetinputs.differentiable(ss) + # always use two-sided differentiation for hetinputs + differentiable_hetinputs = self.hetinputs.differentiable(ss, h, True) differentiable_hetoutputs = None if self.hetoutputs is not None: - differentiable_hetoutputs = self.hetoutputs.differentiable(ss) + differentiable_hetoutputs = self.hetoutputs.differentiable(ss, h, twosided) ss = ss.copy() for k in self.backward: ss[k + '_p'] = exog.expectation(ss[k]) - differentiable_backward_fun = self.backward_fun.differentiable(ss, h=h) + differentiable_backward_fun = self.backward_fun.differentiable(ss, h, twosided) return differentiable_backward_fun, differentiable_hetinputs, differentiable_hetoutputs @@ -480,9 +481,9 @@ def initialize_backward(self, ss): def make_exog_law_of_motion(self, d:dict): return CombinedTransition([Markov(d[k], i) for i, k in enumerate(self.exogenous)]) - def make_endog_law_of_motion(self, d: dict): + def make_endog_law_of_motion(self, d: dict, monotonic=False): if len(self.policy) == 1: - return lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid']) + return lottery_1d(d[self.policy[0]], d[self.policy[0] + '_grid'], monotonic) else: return lottery_2d(d[self.policy[0]], d[self.policy[1]], - d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid']) \ No newline at end of file + d[self.policy[0] + '_grid'], d[self.policy[1] + '_grid'], monotonic) \ No newline at end of file diff --git a/src/sequence_jacobian/blocks/support/het_support.py b/src/sequence_jacobian/blocks/support/het_support.py index c18c019..499b23f 100644 --- a/src/sequence_jacobian/blocks/support/het_support.py +++ b/src/sequence_jacobian/blocks/support/het_support.py @@ -1,7 +1,7 @@ import numpy as np from . import het_compiled from ...utilities.discretize import stationary as general_stationary -from ...utilities.interpolate import interpolate_coord_robust +from ...utilities.interpolate import interpolate_coord_robust, interpolate_coord from ...utilities.multidim import multiply_ith_dimension from typing import Optional, Sequence, Any, List, Tuple, Union @@ -39,8 +39,11 @@ def expectation_shock(self, shocks): -def lottery_1d(a, a_grid): - return PolicyLottery1D(*interpolate_coord_robust(a_grid, a), a_grid) +def lottery_1d(a, a_grid, monotonic=False): + if not monotonic: + return PolicyLottery1D(*interpolate_coord_robust(a_grid, a), a_grid) + else: + return PolicyLottery1D(*interpolate_coord(a_grid, a), a_grid) class PolicyLottery1D(Transition): @@ -81,9 +84,14 @@ def forward_shock(self, da): return het_compiled.forward_policy_shock_1d(self.Dss, self.i, pi_shock).reshape(self.shape) -def lottery_2d(a, b, a_grid, b_grid): - return PolicyLottery2D(*interpolate_coord_robust(a_grid, a), +def lottery_2d(a, b, a_grid, b_grid, monotonic=False): + if not monotonic: + return PolicyLottery2D(*interpolate_coord_robust(a_grid, a), *interpolate_coord_robust(b_grid, b), a_grid, b_grid) + if monotonic: + # right now we have no monotonic 2D examples, so this shouldn't be called + return PolicyLottery2D(*interpolate_coord(a_grid, a), + *interpolate_coord(b_grid, b), a_grid, b_grid) class PolicyLottery2D(Transition): diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index bfb3dc1..734306f 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -81,20 +81,30 @@ def wrapped_call(self, input_dict, preprocess=None, postprocess=None): return output_dict - def differentiable(self, input_dict, h=1E-6, h2=1E-4): - return DifferentiableExtendedFunction(self.f, self.name, self.inputs, self.outputs, input_dict, h, h2) + def differentiable(self, input_dict, h=1E-5, twosided=False): + return DifferentiableExtendedFunction(self.f, self.name, self.inputs, self.outputs, input_dict, h, twosided) class DifferentiableExtendedFunction(ExtendedFunction): - def __init__(self, f, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4): + def __init__(self, f, name, inputs, outputs, input_dict, h=1E-5, twosided=False): self.f, self.name, self.inputs, self.outputs = f, name, inputs, outputs self.input_dict = input_dict self.output_dict = None # lazy evaluation of outputs for one-sided diff self.h = h - self.h2 = h2 + self.default_twosided = twosided - def diff(self, shock_dict, h=None, hide_zeros=False): + def diff(self, shock_dict, h=None, hide_zeros=False, twosided=None): + if twosided is None: + twosided = self.default_twosided + + if not twosided: + return self.diff1(shock_dict, h, hide_zeros) + else: + return self.diff2(shock_dict, h, hide_zeros) + + + def diff1(self, shock_dict, h=None, hide_zeros=False): if h is None: h = self.h @@ -115,7 +125,7 @@ def diff(self, shock_dict, h=None, hide_zeros=False): def diff2(self, shock_dict, h=None, hide_zeros=False): if h is None: - h = self.h2 + h = self.h shocked_input_dict_up = {**self.input_dict, **{k: self.input_dict[k] + h * shock for k, shock in shock_dict.items() if k in self.input_dict}} @@ -206,24 +216,34 @@ def remove(self, name): def children(self): return OrderedSet(self.functions) - def differentiable(self, input_dict, h=1E-6, h2=1E-4): - return DifferentiableExtendedParallelFunction(self.functions, self.name, self.inputs, self.outputs, input_dict, h, h2) + def differentiable(self, input_dict, h=1E-5, twosided=False): + return DifferentiableExtendedParallelFunction(self.functions, self.name, self.inputs, self.outputs, input_dict, h, twosided) class DifferentiableExtendedParallelFunction(ExtendedParallelFunction, DifferentiableExtendedFunction): - def __init__(self, functions, name, inputs, outputs, input_dict, h=1E-6, h2=1E-4): + def __init__(self, functions, name, inputs, outputs, input_dict, h=1E-5, twosided=False): self.name, self.inputs, self.outputs = name, inputs, outputs diff_functions = {} for k, f in functions.items(): - diff_functions[k] = f.differentiable(input_dict, h, h2) + diff_functions[k] = f.differentiable(input_dict, h) self.diff_functions = diff_functions + self.default_twosided = twosided - def diff(self, shock_dict, h=None, outputs=None, hide_zeros=False): + def diff(self, shock_dict, h=None, outputs=None, hide_zeros=False, twosided=False): + if twosided is None: + twosided = self.default_twosided + + if not twosided: + return self.diff1(shock_dict, h, outputs, hide_zeros) + else: + return self.diff2(shock_dict, h, outputs, hide_zeros) + + def diff1(self, shock_dict, h=None, outputs=None, hide_zeros=False): results = {} for f in self.diff_functions.values(): if not f.inputs.isdisjoint(shock_dict): if outputs is None or not f.outputs.isdisjoint(outputs): - results.update(f.diff(shock_dict, h, hide_zeros)) + results.update(f.diff1(shock_dict, h, hide_zeros)) if outputs is not None: results = {k: results[k] for k in outputs if k in results} return results From dbb2a8021c5fae72ac13d82a0256fb5d880cf24a Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 21 Sep 2021 15:51:27 -0500 Subject: [PATCH 280/288] now H_U_factored keyword argument in dynamic solve_ methods, allowing direct usage and also solvedblocks to use stored factored jacobians that are in Js --- src/sequence_jacobian/blocks/block.py | 35 +++++++++++++------ src/sequence_jacobian/blocks/het_block.py | 4 +-- src/sequence_jacobian/blocks/solved_block.py | 12 +++++-- .../classes/jacobian_dict.py | 6 +++- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 0fd90cc..733fc58 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -189,7 +189,7 @@ def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=tar def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, internals: Union[Dict[str, List[str]], List[str]] = {}, Js: Dict[str, JacobianDict] = {}, - options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + options: Dict[str, dict] = {}, H_U_factored: Optional[FactoredJacobianDict] = None, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -202,8 +202,10 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ T = inputs.T Js = self.partial_jacobians(ss, input_names | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) - H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs) - H_U_factored = FactoredJacobianDict(H_U, T) + + if H_U_factored is None: + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs) + H_U_factored = FactoredJacobianDict(H_U, T) opts = self.get_options(options, kwargs, 'solve_impulse_nonlinear') @@ -231,7 +233,9 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Optional[Dict[str, JacobianDict]] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + Js: Optional[Dict[str, JacobianDict]] = {}, options: Dict[str, dict] = {}, + H_U_factored: Optional[FactoredJacobianDict] = None, **kwargs) -> ImpulseDict: + """Calculate a general equilibrium, linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -245,9 +249,13 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets Js = self.partial_jacobians(ss, input_names | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) - H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) - dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).get(targets).pack() # .get(targets) fills in zeros - dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH), unknowns, T) + dH = self.impulse_linear(ss, inputs, targets, Js, options, **kwargs).get(targets) # .get(targets) fills in zeros + + if H_U_factored is None: + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) + dU = ImpulseDict.unpack(-np.linalg.solve(H_U, dH.pack()), unknowns, T) + else: + dU = H_U_factored @ dH return (inputs | dU)[inputs_as_outputs] | self.impulse_linear(ss, dU | inputs, actual_outputs, Js, options, **kwargs) @@ -255,7 +263,8 @@ def solve_impulse_linear(self, ss: SteadyStateDict, unknowns: List[str], targets def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: List[str], outputs: Optional[List[str]] = None, T: int = 300, - Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, + H_U_factored: Optional[FactoredJacobianDict] = None, **kwargs) -> JacobianDict: """Calculate a general equilibrium Jacobian to a set of `exogenous` shocks at a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" @@ -264,9 +273,13 @@ def solve_jacobian(self, ss: SteadyStateDict, unknowns: List[str], targets: List Js = self.partial_jacobians(ss, inputs | unknowns, (actual_outputs | targets) - unknowns, T, Js, options, **kwargs) - H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) - H_Z = self.jacobian(ss, inputs, targets, T, Js, options, **kwargs).pack(T) - U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z), unknowns, inputs, T) + H_Z = self.jacobian(ss, inputs, targets, T, Js, options, **kwargs) + + if H_U_factored is None: + H_U = self.jacobian(ss, unknowns, targets, T, Js, options, **kwargs).pack(T) + U_Z = JacobianDict.unpack(-np.linalg.solve(H_U, H_Z.pack(T)), unknowns, inputs, T) + else: + U_Z = H_U_factored @ H_Z from sequence_jacobian import combine self_with_unknowns = combine([U_Z, self]) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 5f101d6..8ce6c95 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -128,8 +128,8 @@ def _impulse_nonlinear(self, ssin, inputs, outputs, internals, monotonic=False): internals_dict = {self.name: {k: individual_paths[k] for k in internals}} return ImpulseDict(aggregates, internals_dict, inputs.T) - ssin - def _impulse_linear(self, ss, inputs, outputs, Js): - return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js).apply(inputs)) + def _impulse_linear(self, ss, inputs, outputs, Js, h=1E-4, twosided=False): + return ImpulseDict(self.jacobian(ss, list(inputs.keys()), outputs, inputs.T, Js, h=h, twosided=twosided).apply(inputs)) def _jacobian(self, ss, inputs, outputs, T, h=1E-4, twosided=False): ss = self.extract_ss_dict(ss) diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index f306856..4d5b2bd 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -71,15 +71,15 @@ def _steady_state(self, calibration, dissolve, options, **kwargs): def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, **kwargs): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs, internals, Js, options, **kwargs) + inputs, outputs, internals, Js, options, self._get_H_U_factored(Js), **kwargs) def _impulse_linear(self, ss, inputs, outputs, Js, options): return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs, Js, options) + inputs, outputs, Js, options, self._get_H_U_factored(Js)) def _jacobian(self, ss, inputs, outputs, T, Js, options): return self.block.solve_jacobian(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs, T, Js, options)[outputs] + inputs, outputs, T, Js, options, self._get_H_U_factored(Js))[outputs] def _partial_jacobians(self, ss, inputs, outputs, T, Js, options): # call it on the child first @@ -91,3 +91,9 @@ def _partial_jacobians(self, ss, inputs, outputs, T, Js, options): H_U_factored = FactoredJacobianDict(H_U, T) return {**inner_Js, self.name: H_U_factored} + + def _get_H_U_factored(self, Js): + if self.name in Js and isinstance(Js[self.name], FactoredJacobianDict): + return Js[self.name] + else: + return None diff --git a/src/sequence_jacobian/classes/jacobian_dict.py b/src/sequence_jacobian/classes/jacobian_dict.py index aeda2e4..91b3a24 100644 --- a/src/sequence_jacobian/classes/jacobian_dict.py +++ b/src/sequence_jacobian/classes/jacobian_dict.py @@ -231,6 +231,10 @@ def unpack(bigjac, outputs, inputs, T): jacdict[O][I] = bigjac[(T * iO):(T * (iO + 1)), (T * iI):(T * (iI + 1))] return JacobianDict(jacdict, outputs, inputs, T=T) + def factored(self, T=None): + return FactoredJacobianDict(self, T) + + class FactoredJacobianDict: def __init__(self, jacobian_dict: JacobianDict, T=None): if jacobian_dict.T is None: @@ -284,7 +288,7 @@ def compose(self, J: JacobianDict): def apply(self, x: Union[ImpulseDict, Dict[str, Array]]): """Returns -H_U^{-1} @ x""" - xsub = ImpulseDict(x)[self.targets].pack() + xsub = ImpulseDict(x).get(self.targets).pack() out = -factored_solve(self.H_U_factored, xsub) return ImpulseDict.unpack(out, self.unknowns, self.T) From 164d5e54cd2499f18be35bdb429b7eb2acb1383c Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Tue, 21 Sep 2021 16:53:57 -0500 Subject: [PATCH 281/288] added ss_initial as an option for impulse_nonlinear and rewrote Displace object to accommodate this, fixed bugs for existing tests but have not tested yet --- src/sequence_jacobian/blocks/block.py | 16 ++-- .../blocks/combined_block.py | 8 +- src/sequence_jacobian/blocks/het_block.py | 5 +- src/sequence_jacobian/blocks/simple_block.py | 15 +++- src/sequence_jacobian/blocks/solved_block.py | 4 +- .../blocks/support/simple_displacement.py | 82 ++++++++++--------- src/sequence_jacobian/utilities/bijection.py | 4 +- tests/base/test_displacement_handlers.py | 29 +++---- 8 files changed, 92 insertions(+), 71 deletions(-) diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 733fc58..9d19308 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -59,20 +59,21 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, internals: Union[Dict[str, List[str]], List[str]] = {}, - Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, + ss_initial: Optional[SteadyStateDict] = None, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'impulse_nonlinear') inputs = ImpulseDict(inputs) actual_outputs, inputs_as_outputs = self.process_outputs(ss, self.make_ordered_set(inputs), self.make_ordered_set(outputs)) - # SolvedBlocks may use Js and may be nested in a CombinedBlock if isinstance(self, Parent): - out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, internals, Js, options, **own_options) + # SolvedBlocks may use Js and may be nested in a CombinedBlock, so we need to pass them down to any parent + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, internals, Js, options, self.M.inv @ ss_initial, **own_options) elif hasattr(self, 'internals'): - out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, self.internals_to_report(internals), **own_options) + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, self.internals_to_report(internals), self.M.inv @ ss_initial, **own_options) else: - out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, **own_options) + out = self.M @ self._impulse_nonlinear(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ actual_outputs, self.M.inv @ ss_initial, **own_options) return inputs[inputs_as_outputs] | out @@ -189,7 +190,8 @@ def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=tar def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targets: List[str], inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, internals: Union[Dict[str, List[str]], List[str]] = {}, Js: Dict[str, JacobianDict] = {}, - options: Dict[str, dict] = {}, H_U_factored: Optional[FactoredJacobianDict] = None, **kwargs) -> ImpulseDict: + options: Dict[str, dict] = {}, H_U_factored: Optional[FactoredJacobianDict] = None, + ss_initial: Optional[SteadyStateDict] = None, **kwargs) -> ImpulseDict: """Calculate a general equilibrium, non-linear impulse response to a set of shocks in `inputs` around a steady state `ss`, given a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the `targets` that must hold in general equilibrium""" @@ -214,7 +216,7 @@ def solve_impulse_nonlinear(self, ss: SteadyStateDict, unknowns: List[str], targ if opts['verbose']: print(f'Solving {self.name} for {unknowns} to hit {targets}') for it in range(opts['maxit']): - results = self.impulse_nonlinear(ss, inputs | U, actual_outputs | targets, internals, Js, options, **kwargs) + results = self.impulse_nonlinear(ss, inputs | U, actual_outputs | targets, internals, Js, options, ss_initial, **kwargs) errors = {k: np.max(np.abs(results[k])) for k in targets} if opts['verbose']: print(f'On iteration {it}') diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 2903934..244b009 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -69,7 +69,7 @@ def _steady_state(self, calibration, dissolve, **kwargs): return ss - def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options): + def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, ss_initial): original_outputs = outputs outputs = (outputs | self._required) - ss._vector_valued() @@ -77,8 +77,10 @@ def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options): for block in self.blocks: input_args = {k: v for k, v in impulses.items() if k in block.inputs} - if input_args: # If this block is actually perturbed - impulses.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, internals, Js, options)) + if input_args or ss_initial is not None: + # If this block is actually perturbed, or we start from different initial ss + # TODO: be more selective about ss_initial here - did any inputs change that matter for this one block? + impulses.update(block.impulse_nonlinear(ss, input_args, outputs & block.outputs, internals, Js, options, ss_initial)) return ImpulseDict({k: impulses.toplevel[k] for k in original_outputs if k in impulses.toplevel}, impulses.internals, impulses.T) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index 8ce6c95..a2b340d 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -106,8 +106,11 @@ def _steady_state(self, calibration, backward_tol=1E-8, backward_maxit=5000, return SteadyStateDict({k: ss[k] for k in ss if k not in self.internals}, {self.name: {k: ss[k] for k in ss if k in self.internals}}) - def _impulse_nonlinear(self, ssin, inputs, outputs, internals, monotonic=False): + def _impulse_nonlinear(self, ssin, inputs, outputs, internals, ss_initial, monotonic=False): ss = self.extract_ss_dict(ssin) + if ss_initial is not None: + # only effect of distinct initial ss on hetblock is different initial distribution + ss['Dbeg'] = ss_initial['Dbeg'] # identify individual variable paths we want from backward iteration, then run it toreturn = self.non_backward_outputs diff --git a/src/sequence_jacobian/blocks/simple_block.py b/src/sequence_jacobian/blocks/simple_block.py index 1d38543..c542c80 100644 --- a/src/sequence_jacobian/blocks/simple_block.py +++ b/src/sequence_jacobian/blocks/simple_block.py @@ -45,16 +45,25 @@ def _steady_state(self, ss): outputs = self.f.wrapped_call(ss, preprocess=ignore, postprocess=misc.numeric_primitive) return SteadyStateDict({**ss, **outputs}) - def _impulse_nonlinear(self, ss, inputs, outputs): + def _impulse_nonlinear(self, ss, inputs, outputs, ss_initial): + if ss_initial is None: + ss_initial = ss + ss_initial_flag = False + else: + ss_initial_flag = True + input_args = {} for k, v in inputs.items(): if np.isscalar(v): raise ValueError(f'Keyword argument {k}={v} is scalar, should be time path.') - input_args[k] = Displace(v + ss[k], ss=ss[k], name=k) + input_args[k] = Displace(v + ss[k], ss[k], ss_initial[k], k) for k in self.inputs: if k not in input_args: - input_args[k] = ignore(ss[k]) + if not ss_initial_flag or (ss_initial_flag and np.array_equal(ss_initial[k], ss[k])): + input_args[k] = ignore(ss[k]) + else: + input_args[k] = Displace(np.full(inputs.T, ss[k]), ss[k], ss_initial[k], k) return ImpulseDict(make_impulse_uniform_length(self.f(input_args)))[outputs] - ss diff --git a/src/sequence_jacobian/blocks/solved_block.py b/src/sequence_jacobian/blocks/solved_block.py index 4d5b2bd..048f1ad 100644 --- a/src/sequence_jacobian/blocks/solved_block.py +++ b/src/sequence_jacobian/blocks/solved_block.py @@ -69,9 +69,9 @@ def _steady_state(self, calibration, dissolve, options, **kwargs): return self.block.solve_steady_state(calibration, unknowns, self.targets, options, **kwargs) - def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, **kwargs): + def _impulse_nonlinear(self, ss, inputs, outputs, internals, Js, options, ss_initial, **kwargs): return self.block.solve_impulse_nonlinear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), - inputs, outputs, internals, Js, options, self._get_H_U_factored(Js), **kwargs) + inputs, outputs, internals, Js, options, self._get_H_U_factored(Js), ss_initial, **kwargs) def _impulse_linear(self, ss, inputs, outputs, Js, options): return self.block.solve_impulse_linear(ss, OrderedSet(self.unknowns), OrderedSet(self.targets), diff --git a/src/sequence_jacobian/blocks/support/simple_displacement.py b/src/sequence_jacobian/blocks/support/simple_displacement.py index 0ace468..62959c5 100644 --- a/src/sequence_jacobian/blocks/support/simple_displacement.py +++ b/src/sequence_jacobian/blocks/support/simple_displacement.py @@ -276,14 +276,17 @@ class Displace(np.ndarray): """This class makes time displacements of a time path, given the steady-state value. Needed for SimpleBlock.td()""" - def __new__(cls, x, ss=None, name='UNKNOWN'): + def __new__(cls, x, ss=None, ss_initial=None, name='UNKNOWN'): obj = np.asarray(x).view(cls) obj.ss = ss + obj.ss_initial = ss_initial obj.name = name return obj def __array_finalize__(self, obj): + # note by Matt: not sure what this does? self.ss = getattr(obj, "ss", None) + self.ss_initial = getattr(obj, "ss_initial", None) self.name = getattr(obj, "name", "UNKNOWN") def __repr__(self): @@ -291,162 +294,161 @@ def __repr__(self): # TODO: Implemented a very preliminary generalization of Displace to higher-dimensional (>1) ndarrays # however the rigorous operator overloading/testing has not been checked for higher dimensions. - # Should also implement some checks for the dimension of .ss, to ensure that it's always N-1 - # where we also assume that the *last* dimension is the time dimension + # (Matt: fixed so that it's the first dimension that is time dimension, consistent with everything else) def __call__(self, index): if index != 0: if self.ss is None: raise KeyError(f'Trying to call {self.name}({index}), but steady-state {self.name} not given!') newx = np.zeros(np.shape(self)) if index > 0: - newx[..., :-index] = numeric_primitive(self)[..., index:] - newx[..., -index:] = self.ss + newx[:-index] = numeric_primitive(self)[index:] + newx[-index:] = self.ss else: - newx[..., -index:] = numeric_primitive(self)[..., :index] - newx[..., :-index] = self.ss - return Displace(newx, ss=self.ss) + newx[-index:] = numeric_primitive(self)[:index] + newx[:-index] = self.ss_initial + return Displace(newx, self.ss, self.ss_initial) else: return self def apply(self, f, **kwargs): - return Displace(f(numeric_primitive(self), **kwargs), ss=f(self.ss)) + return Displace(f(numeric_primitive(self), **kwargs), ss=f(self.ss), ss_initial=f(self.ss_initial)) def __pos__(self): return self def __neg__(self): - return Displace(-numeric_primitive(self), ss=-self.ss) + return Displace(-numeric_primitive(self), ss=-self.ss, ss_initial=-self.ss_initial) def __add__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss + other.ss) + ss=self.ss + other.ss, ss_initial=self.ss_initial + other.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss + numeric_primitive(other)) + ss=self.ss + numeric_primitive(other), ss_initial=self.ss_initial + numeric_primitive(other)) else: # TODO: See if there is a different, systematic way we want to handle this case. warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(self) + numeric_primitive(other), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __radd__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=other.ss + self.ss) + ss=other.ss + self.ss, ss_initial=other.ss_initial + self.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=numeric_primitive(other) + self.ss) + ss=numeric_primitive(other) + self.ss, ss_initial=numeric_primitive(other) + self.ss_initial) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(other) + numeric_primitive(self), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __sub__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss - other.ss) + ss=self.ss - other.ss, ss_initial=self.ss_initial - other.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss - numeric_primitive(other)) + ss=self.ss - numeric_primitive(other), ss_initial=self.ss_initial - numeric_primitive(other)) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(self) - numeric_primitive(other), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __rsub__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=other.ss - self.ss) + ss=other.ss - self.ss, ss_initial=other.ss_initial - self.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=numeric_primitive(other) - self.ss) + ss=numeric_primitive(other) - self.ss, ss_initial=numeric_primitive(other) - self.ss_initial) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(other) - numeric_primitive(self), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __mul__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss * other.ss) + ss=self.ss * other.ss, ss_initial=self.ss_initial * other.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss * numeric_primitive(other)) + ss=self.ss * numeric_primitive(other), ss_initial=self.ss_initial * numeric_primitive(other)) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(self) * numeric_primitive(other), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __rmul__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=other.ss * self.ss) + ss=other.ss * self.ss, ss_initial=other.ss_initial * self.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=numeric_primitive(other) * self.ss) + ss=numeric_primitive(other) * self.ss, ss_initial=numeric_primitive(other) * self.ss_initial) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(other) * numeric_primitive(self), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __truediv__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss / other.ss) + ss=self.ss / other.ss, ss_initial=self.ss_initial / other.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss / numeric_primitive(other)) + ss=self.ss / numeric_primitive(other), ss_initial=self.ss_initial / numeric_primitive(other)) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(self) / numeric_primitive(other), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __rtruediv__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=other.ss / self.ss) + ss=other.ss / self.ss, ss_initial=other.ss_initial / self.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=numeric_primitive(other) / self.ss) + ss=numeric_primitive(other) / self.ss, ss_initial=numeric_primitive(other) / self.ss_initial) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(other) / numeric_primitive(self), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __pow__(self, power): if isinstance(power, Displace): return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss ** power.ss) + ss=self.ss ** power.ss, ss_initial=self.ss_initial ** power.ss_initial) elif np.isscalar(power): return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss ** numeric_primitive(power)) + ss=self.ss ** numeric_primitive(power), ss_initial=self.ss_initial ** numeric_primitive(power)) else: warn("\n" + f"Applying operation to {power}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(self) ** numeric_primitive(power), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) def __rpow__(self, other): if isinstance(other, Displace): return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=other.ss ** self.ss) + ss=other.ss ** self.ss, ss_initial=other.ss_initial ** self.ss_initial) elif np.isscalar(other): return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=numeric_primitive(other) ** self.ss) + ss=numeric_primitive(other) ** self.ss, ss_initial=numeric_primitive(other) ** self.ss_initial) else: warn("\n" + f"Applying operation to {other}, a vector, and {self}, a Displace." + "\n" + f"The resulting Displace object will retain the steady-state value of the original Displace object.") return Displace(numeric_primitive(other) ** numeric_primitive(self), - ss=self.ss) + ss=self.ss, ss_initial=self.ss_initial) class AccumulatedDerivative: diff --git a/src/sequence_jacobian/utilities/bijection.py b/src/sequence_jacobian/utilities/bijection.py index cb889bd..48ea392 100644 --- a/src/sequence_jacobian/utilities/bijection.py +++ b/src/sequence_jacobian/utilities/bijection.py @@ -25,7 +25,9 @@ def __getitem__(self, k): return self.map.get(k, k) def __matmul__(self, x): - if isinstance(x, str): + if x is None: + return None + elif isinstance(x, str): return self[x] elif isinstance(x, Bijection): # compose self: v -> u with x: w -> v diff --git a/tests/base/test_displacement_handlers.py b/tests/base/test_displacement_handlers.py index 1e28d9a..9080922 100644 --- a/tests/base/test_displacement_handlers.py +++ b/tests/base/test_displacement_handlers.py @@ -121,26 +121,28 @@ def test_ignore_vector(): def test_displace(): + # TODO: test ss_initial being different from ss + # Test unary operations - arg_singles = [Displace(np.array([1, 2, 3]), ss=2), Displace(np.array([1, 2, 3]), ss=2)(-1)] + arg_singles = [Displace(np.array([1, 2, 3]), 2, 2), Displace(np.array([1, 2, 3]), 2, 2)(-1)] for t1 in arg_singles: for op in ["__neg__", "__pos__"]: assert type(apply_op(op, t1)) == Displace assert np.all(numeric_primitive(apply_op(op, t1)) == apply_op(op, numeric_primitive(t1))) # Test binary operations - arg_pairs = [(Displace(np.array([1, 2, 3]), ss=2), 1), - (Displace(np.array([1, 2, 3]), ss=2), IgnoreFloat(1)), - (Displace(np.array([1, 2, 3]), ss=2), Displace(np.array([2, 3, 4]), ss=3)), - (1, Displace(np.array([1, 2, 3]), ss=2)), - (IgnoreFloat(1), Displace(np.array([1, 2, 3]), ss=2)), - - (Displace(np.array([1, 2, 3]), ss=2)(-1), 1), - (Displace(np.array([1, 2, 3]), ss=2)(-1), IgnoreFloat(1)), - (Displace(np.array([1, 2, 3]), ss=2)(-1), Displace(np.array([2, 3, 4]), ss=3)), - (Displace(np.array([1, 2, 3]), ss=2), Displace(np.array([2, 3, 4]), ss=3)(-1)), - (1, Displace(np.array([1, 2, 3]), ss=2)(-1)), - (IgnoreFloat(1), Displace(np.array([1, 2, 3]), ss=2)(-1))] + arg_pairs = [(Displace(np.array([1, 2, 3]), 2, 2), 1), + (Displace(np.array([1, 2, 3]), 2, 2), IgnoreFloat(1)), + (Displace(np.array([1, 2, 3]), 2, 2), Displace(np.array([2, 3, 4]), 3, 3)), + (1, Displace(np.array([1, 2, 3]), 2, 2)), + (IgnoreFloat(1), Displace(np.array([1, 2, 3]), 2, 2)), + + (Displace(np.array([1, 2, 3]), 2, 2)(-1), 1), + (Displace(np.array([1, 2, 3]), 2, 2)(-1), IgnoreFloat(1)), + (Displace(np.array([1, 2, 3]), 2, 2)(-1), Displace(np.array([2, 3, 4]), 3, 3)), + (Displace(np.array([1, 2, 3]), 2, 2), Displace(np.array([2, 3, 4]), 3, 3)(-1)), + (1, Displace(np.array([1, 2, 3]), 2, 2)(-1)), + (IgnoreFloat(1), Displace(np.array([1, 2, 3]), 2, 2)(-1))] for pair in arg_pairs: t1, t2 = pair for op in ["__add__", "__radd__", "__sub__", "__rsub__", "__mul__", "__rmul__", @@ -159,7 +161,6 @@ def test_displace(): t1_manual_displace[-1:] = t1.ss assert np.all(numeric_primitive(t1(1)) == t1_manual_displace) - def test_accumulated_derivative(): # Test unary operations arg_singles = [AccumulatedDerivative(), AccumulatedDerivative()(-1), AccumulatedDerivative(elements={(1, 1): 2.}, f_value=2.)] From 74d0ff29dce68fef437fb6ca2a9cd86a3f3072f8 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Thu, 23 Sep 2021 11:29:05 -0500 Subject: [PATCH 282/288] refactored graph utilities (helper ones still mostly unchanged), with eye toward using in a ExtendedCombinedFunction class and maybe also improving CombinedBlock --- src/sequence_jacobian/utilities/function.py | 7 +- src/sequence_jacobian/utilities/graph.py | 126 +++++++++----------- 2 files changed, 60 insertions(+), 73 deletions(-) diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 734306f..e7e4f99 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -2,7 +2,8 @@ import inspect import numpy as np -from sequence_jacobian.utilities.ordered_set import OrderedSet +from .ordered_set import OrderedSet +from . import graph # TODO: fix this, have it twice (main version in misc) due to circular import problem # let's make everything point to here for input_list, etc. so that this is unnecessary @@ -258,3 +259,7 @@ def diff2(self, shock_dict, h=None, outputs=None, hide_zeros=False): results = {k: results[k] for k in outputs if k in results} return results + +# class ExtendedCombinedFunction(ExtendedFunction): +# def __init__(self, fs, name=None): + \ No newline at end of file diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index 15f632d..a4aca41 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -1,7 +1,7 @@ """Topological sort and related code""" -def block_sort(blocks, return_io=False): +def block_sort(blocks): """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's @@ -9,36 +9,21 @@ def block_sort(blocks, return_io=False): blocks: `list` A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `blocks` """ - # TODO: Decide whether we want to break out the input and output argument tracking and return to - # a different function... currently it's very convenient to slot it into block_sort directly, but it - # does clutter up the function body - if return_io: - # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map(blocks, return_output_args=True) - - # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph(blocks, outmap, return_input_args=True) + inmap = get_input_map(blocks) + outmap = get_output_map(blocks) + adj = get_block_adjacency_list(blocks, inmap) + revadj = get_block_reverse_adjacency_list(blocks, outmap) - return topological_sort(dep), inargs, outargs - else: - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map(blocks) - - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph(blocks, outmap) - - return topological_sort(dep) + return topological_sort(adj, revadj) -def topological_sort(dep, names=None): +def topological_sort(adj, revadj, names=None): """Given directed graph pointing from each node to the nodes it depends on, topologically sort nodes""" - # get complete set version of dep, and its reversal, and build initial stack of nodes with no dependencies - dep, revdep = complete_reverse_graph(dep) - nodeps = [n for n in dep if not dep[n]] + #dep, revdep = complete_reverse_graph(dep) + dep, revdep = revadj, adj + nodeps = [n for n, depset in enumerate(dep) if not depset] topsorted = [] # Kahn's algorithm: find something with no dependency, delete its edges and update @@ -60,66 +45,61 @@ def topological_sort(dep, names=None): return topsorted -def construct_output_map(blocks, return_output_args=False): - """Construct a map of outputs to the indices of the blocks that produce them. +def get_input_map(blocks: list): + """inmap[i] gives set of block numbers where i is an input""" + inmap = dict() + for num, block in enumerate(blocks): + for i in block.inputs: + inset = inmap.setdefault(i, set()) + inset.add(num) - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - return_output_args: `bool` - A boolean indicating whether to track and return the full set of output arguments of all of the blocks - in `blocks` - """ + return inmap + + +def get_output_map(blocks: list): + """outmap[o] gives unique block number where o is an output""" outmap = dict() - outargs = set() for num, block in enumerate(blocks): - # Find the relevant set of outputs corresponding to a block - if hasattr(block, "outputs"): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: + for o in block.outputs: if o in outmap: raise ValueError(f'{o} is output twice') outmap[o] = num - if return_output_args: - return outmap, outargs - else: - return outmap + return outmap -def construct_dependency_graph(blocks, outmap, return_input_args=False): - """Construct a dependency graph dictionary, with block indices as keys and a set of block indices as values, where - this set is the set of blocks that the key block is dependent on. +def get_block_adjacency_list(blocks, inmap): + """adj[n] for block number n gives set of block numbers which this block points to""" + adj = [] + for block in blocks: + current_adj = set() + for o in block.outputs: + # for each output, if that output is used as an input by some blocks, add those blocks to adj + if o in inmap: + current_adj |= inmap[o] + adj.append(current_adj) + return adj - outmap is the output map (output to block index mapping) created by construct_output_map. - """ - dep = {num: set() for num in range(len(blocks))} - inargs = set() - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: +def get_block_reverse_adjacency_list(blocks, outmap): + """revadj[n] for block number n gives set of block numbers that point to this block""" + revadj = [] + for block in blocks: + current_revadj = set() + for i in block.inputs: if i in outmap: - dep[num].add(outmap[i]) - if return_input_args: - return dep, inargs - else: - return dep + current_revadj.add(outmap[i]) + revadj.append(current_revadj) + return revadj -def find_intermediate_inputs(blocks, **kwargs): +def find_intermediate_inputs(blocks): + # TODO: should be deprecated """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. """ required = set() - outmap = construct_output_map(blocks, **kwargs) + outmap = get_output_map(blocks) for num, block in enumerate(blocks): if hasattr(block, 'inputs'): inputs = block.inputs @@ -168,7 +148,7 @@ def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, helper_blocks=helper_blocks, calibration=calibration) - return topological_sort(dep), inargs, outargs + return topological_sort(map_to_list(complete_reverse_graph(dep)), map_to_list(dep)), inargs, outargs else: # step 1: map outputs to blocks for topological sort outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) @@ -176,7 +156,11 @@ def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io # step 2: dependency graph for topological sort and input list dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) - return topological_sort(dep) + return topological_sort(map_to_list(complete_reverse_graph(dep)), map_to_list(dep)) + + +def map_to_list(m): + return [m[i] for i in range(len(m))] def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): @@ -286,9 +270,7 @@ def complete_reverse_graph(gph): n2_edges = revgph.setdefault(n2, set()) n2_edges.add(n) - gph_missing_n = revgph.keys() - gph.keys() - gph = {**{k: set(v) for k, v in gph.items()}, **{n: set() for n in gph_missing_n}} - return gph, revgph + return revgph def find_cycle(dep, onlyset=None): From 4378979a59d72f99e1bd1612804b2870852d5fa3 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 29 Sep 2021 11:56:25 -0500 Subject: [PATCH 283/288] new graph.DAG class to represent structure of a DAG with inputs and outputs, used this in preliminary CombinedExtendedFunction class --- .../blocks/combined_block.py | 6 +- src/sequence_jacobian/utilities/bijection.py | 2 +- src/sequence_jacobian/utilities/function.py | 111 +++++++++++++++++- src/sequence_jacobian/utilities/graph.py | 77 ++++++++++-- .../utilities/ordered_set.py | 9 +- tests/base/test_jacobian.py | 2 +- tests/base/test_jacobian_dict_block.py | 2 +- tests/base/test_transitional_dynamics.py | 6 +- tests/utils/test_DAG.py | 41 +++++++ tests/utils/test_function.py | 29 +++-- 10 files changed, 248 insertions(+), 37 deletions(-) create mode 100644 tests/utils/test_DAG.py diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 244b009..14e994a 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -28,10 +28,10 @@ class CombinedBlock(Block, Parent): def __init__(self, blocks, name="", model_alias=False, sorted_indices=None, intermediate_inputs=None): super().__init__() - self._blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] - self._sorted_indices = block_sort(blocks) if sorted_indices is None else sorted_indices + blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] + sorted_indices = block_sort(blocks) if sorted_indices is None else sorted_indices self._required = find_intermediate_inputs(blocks) if intermediate_inputs is None else intermediate_inputs - self.blocks = [self._blocks_unsorted[i] for i in self._sorted_indices] + self.blocks = [blocks_unsorted[i] for i in sorted_indices] if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" diff --git a/src/sequence_jacobian/utilities/bijection.py b/src/sequence_jacobian/utilities/bijection.py index 48ea392..0738688 100644 --- a/src/sequence_jacobian/utilities/bijection.py +++ b/src/sequence_jacobian/utilities/bijection.py @@ -27,7 +27,7 @@ def __getitem__(self, k): def __matmul__(self, x): if x is None: return None - elif isinstance(x, str): + elif isinstance(x, str) or isinstance(x, int): return self[x] elif isinstance(x, Bijection): # compose self: v -> u with x: w -> v diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index e7e4f99..462fdc2 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -94,7 +94,6 @@ def __init__(self, f, name, inputs, outputs, input_dict, h=1E-5, twosided=False) self.h = h self.default_twosided = twosided - def diff(self, shock_dict, h=None, hide_zeros=False, twosided=None): if twosided is None: twosided = self.default_twosided @@ -104,7 +103,6 @@ def diff(self, shock_dict, h=None, hide_zeros=False, twosided=None): else: return self.diff2(shock_dict, h, hide_zeros) - def diff1(self, shock_dict, h=None, hide_zeros=False): if h is None: h = self.h @@ -260,6 +258,109 @@ def diff2(self, shock_dict, h=None, outputs=None, hide_zeros=False): return results -# class ExtendedCombinedFunction(ExtendedFunction): -# def __init__(self, fs, name=None): - \ No newline at end of file +class CombinedExtendedFunction(ExtendedFunction): + def __init__(self, fs, name=None): + self.dag = graph.DAG([ExtendedFunction(f) for f in fs]) + self.inputs = self.dag.inputs + self.outputs = self.dag.outputs + self.functions = {b.name: b for b in self.dag.blocks} + + if name is None: + names = list(self.functions) + if len(names) == 1: + self.name = names[0] + else: + self.name = f'{names[0]}_{names[-1]}' + else: + self.name = name + + def __call__(self, input_dict, outputs=None): + functions_to_visit = list(self.functions.values()) + if outputs is not None: + functions_to_visit = [functions_to_visit[i] for i in self.dag.visit_from_outputs(outputs)] + + results = input_dict.copy() + for f in functions_to_visit: + results.update(f(results)) + + if outputs is not None: + return {k: results[k] for k in outputs} + else: + return results + + def call_on_deviations(self, ss, dev_dict, outputs=None): + functions_to_visit = self.filter(list(self.functions.values()), dev_dict, outputs) + + results = {} + input_dict = {**ss, **dev_dict} + for f in functions_to_visit: + out = f(input_dict) + results.update(out) + input_dict.update(out) + + if outputs is not None: + return {k: v for k, v in results.items() if k in outputs} + else: + return results + + def filter(self, function_list, inputs, outputs=None): + nums_to_visit = self.dag.visit_from_inputs(inputs) + if outputs is not None: + nums_to_visit &= self.dag.visit_from_outputs(outputs) + return [function_list[n] for n in nums_to_visit] + + def wrapped_call(self, input_dict, preprocess=None, postprocess=None): + raise NotImplementedError + + def differentiable(self, input_dict, h=1E-5, twosided=False): + return DifferentiableCombinedExtendedFunction(self.functions, self.dag, self.name, self.inputs, self.outputs, input_dict, h, twosided) + + +class DifferentiableCombinedExtendedFunction(CombinedExtendedFunction, DifferentiableExtendedFunction): + def __init__(self, functions, dag, name, inputs, outputs, input_dict, h=1E-5, twosided=False): + self.dag, self.name, self.inputs, self.outputs = dag, name, inputs, outputs + diff_functions = {} + for k, f in functions.items(): + diff_functions[k] = f.differentiable(input_dict, h) + self.diff_functions = diff_functions + self.default_twosided = twosided + + def diff(self, shock_dict, h=None, outputs=None, hide_zeros=False, twosided=False): + if twosided is None: + twosided = self.default_twosided + + if not twosided: + return self.diff1(shock_dict, h, outputs, hide_zeros) + else: + return self.diff2(shock_dict, h, outputs, hide_zeros) + + def diff1(self, shock_dict, h=None, outputs=None, hide_zeros=False): + functions_to_visit = self.filter(list(self.diff_functions.values()), shock_dict, outputs) + + shock_dict = shock_dict.copy() + results = {} + for f in functions_to_visit: + out = f.diff1(shock_dict, h, hide_zeros) + results.update(out) + shock_dict.update(out) + + if outputs is not None: + return {k: v for k, v in results.items() if k in outputs} + else: + return results + + def diff2(self, shock_dict, h=None, outputs=None, hide_zeros=False): + functions_to_visit = self.filter(list(self.diff_functions.values()), shock_dict, outputs) + + shock_dict = shock_dict.copy() + results = {} + for f in functions_to_visit: + out = f.diff2(shock_dict, h, hide_zeros) + results.update(out) + shock_dict.update(out) + + if outputs is not None: + return {k: v for k, v in results.items() if k in outputs} + else: + return results + diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index a4aca41..709a09e 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -1,4 +1,66 @@ """Topological sort and related code""" +from .ordered_set import OrderedSet +from .bijection import Bijection + +class DAG: + """Represents "blocks" that each have inputs and outputs, where output-input relationships between + blocks form a DAG. Fundamental DAG object intended to underlie CombinedBlock and CombinedExtendedFunction. + + Initialized with list of blocks, which are then topologically sorted""" + + def __init__(self, blocks): + inmap = get_input_map(blocks) + outmap = get_output_map(blocks) + adj = get_block_adjacency_list(blocks, inmap) + revadj = get_block_reverse_adjacency_list(blocks, outmap) + topsort = topological_sort(adj, revadj) + + M = Bijection({i: t for i, t in enumerate(topsort)}) + + self.blocks = [blocks[t] for t in topsort] + self.inmap = {k: M @ v for k, v in inmap.items()} + self.outmap = {k: M @ v for k, v in outmap.items()} + self.adj = [M @ adj[t] for t in topsort] + self.revadj = [M @ revadj[t] for t in topsort] + + self.inputs = OrderedSet(k for k in inmap if k not in outmap) + self.outputs = OrderedSet(outmap) + + + def visit_from_inputs(self, inputs): + """Which block numbers are ultimately dependencies of 'inputs'?""" + inputs = inputs & self.inputs + visited = OrderedSet() + for n, (block, parentset) in enumerate(zip(self.blocks, self.revadj)): + # first see if block has its input directly changed + for i in inputs: + if i in block.inputs: + visited.add(n) + break + else: + if not parentset.isdisjoint(visited): + visited.add(n) + + return visited + + def visit_from_outputs(self, outputs): + """Which block numbers are 'outputs' ultimately dependent on?""" + outputs = outputs & self.outputs + visited = OrderedSet() + for n in reversed(range(len(self.blocks))): + block = self.blocks[n] + childset = self.adj[n] + + # first see if block has its output directly used + for o in outputs: + if o in block.outputs: + visited.add(n) + break + else: + if not childset.isdisjoint(visited): + visited.add(n) + + return reversed(visited) def block_sort(blocks): @@ -14,15 +76,14 @@ def block_sort(blocks): outmap = get_output_map(blocks) adj = get_block_adjacency_list(blocks, inmap) revadj = get_block_reverse_adjacency_list(blocks, outmap) - return topological_sort(adj, revadj) def topological_sort(adj, revadj, names=None): """Given directed graph pointing from each node to the nodes it depends on, topologically sort nodes""" # get complete set version of dep, and its reversal, and build initial stack of nodes with no dependencies - #dep, revdep = complete_reverse_graph(dep) - dep, revdep = revadj, adj + revdep = adj + dep = [s.copy() for s in revadj] nodeps = [n for n, depset in enumerate(dep) if not depset] topsorted = [] @@ -50,7 +111,7 @@ def get_input_map(blocks: list): inmap = dict() for num, block in enumerate(blocks): for i in block.inputs: - inset = inmap.setdefault(i, set()) + inset = inmap.setdefault(i, OrderedSet()) inset.add(num) return inmap @@ -72,7 +133,7 @@ def get_block_adjacency_list(blocks, inmap): """adj[n] for block number n gives set of block numbers which this block points to""" adj = [] for block in blocks: - current_adj = set() + current_adj = OrderedSet() for o in block.outputs: # for each output, if that output is used as an input by some blocks, add those blocks to adj if o in inmap: @@ -85,7 +146,7 @@ def get_block_reverse_adjacency_list(blocks, outmap): """revadj[n] for block number n gives set of block numbers that point to this block""" revadj = [] for block in blocks: - current_revadj = set() + current_revadj = OrderedSet() for i in block.inputs: if i in outmap: current_revadj.add(outmap[i]) @@ -98,13 +159,13 @@ def find_intermediate_inputs(blocks): """Find outputs of the blocks in blocks that are inputs to other blocks in blocks. This is useful to ensure that all of the relevant curlyJ Jacobians (of all inputs to all outputs) are computed. """ - required = set() + required = OrderedSet() outmap = get_output_map(blocks) for num, block in enumerate(blocks): if hasattr(block, 'inputs'): inputs = block.inputs else: - inputs = set(i for o in block for i in block[o]) + inputs = OrderedSet(i for o in block for i in block[o]) for i in inputs: if i in outmap: required.add(i) diff --git a/src/sequence_jacobian/utilities/ordered_set.py b/src/sequence_jacobian/utilities/ordered_set.py index 7d4a7ff..6d3952b 100644 --- a/src/sequence_jacobian/utilities/ordered_set.py +++ b/src/sequence_jacobian/utilities/ordered_set.py @@ -8,7 +8,7 @@ class OrderedSet: See test_misc_support.test_ordered_set() for examples.""" - def __init__(self, members: Iterable): + def __init__(self, members: Iterable = []): self.d = {k: None for k in members} def dict_from(self, s): @@ -17,6 +17,9 @@ def dict_from(self, s): def __iter__(self): return iter(self.d) + def __reversed__(self): + return OrderedSet(list(self)[::-1]) + def __repr__(self): return f"OrderedSet({list(self)})" @@ -36,7 +39,7 @@ def add(self, x): self.d[x] = None def difference(self, s): - return OrderedSet({k: None for k in self if k not in s}) + return OrderedSet(k for k in self if k not in s) def difference_update(self, s): self.d = self.difference(s).d @@ -46,7 +49,7 @@ def discard(self, k): self.d.pop(k, None) def intersection(self, s): - return OrderedSet({k: None for k in self if k in s}) + return OrderedSet(k for k in self if k in s) def intersection_update(self, s): self.d = self.intersection(s).d diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 2fc8d98..6d91073 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -4,7 +4,7 @@ def test_ks_jac(krusell_smith_dag): ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag - household, firm, *_ = ks_model._blocks_unsorted + household, firm = ks_model['household'], ks_model['firm'] T = 10 # Automatically calculate the general equilibrium Jacobian diff --git a/tests/base/test_jacobian_dict_block.py b/tests/base/test_jacobian_dict_block.py index 62c6f08..430cb97 100644 --- a/tests/base/test_jacobian_dict_block.py +++ b/tests/base/test_jacobian_dict_block.py @@ -36,4 +36,4 @@ def test_jacobian_dict_block_combine(rbc_dag): cblock_w_jdict = combine(blocks_w_jdict) # Using `combine` converts JacobianDicts to JacobianDictBlocks - assert isinstance(cblock_w_jdict._blocks_unsorted[1], JacobianDictBlock) + assert isinstance(cblock_w_jdict.blocks[0], JacobianDictBlock) diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 308ca48..0710ca0 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -50,7 +50,7 @@ def test_hank_td(one_asset_hank_dag): hank_model, ss, unknowns, targets, exogenous = one_asset_hank_dag T = 30 - household = hank_model._blocks_unsorted[0] + household = hank_model['household'] J_ha = household.jacobian(ss=ss, T=T, inputs=['Div', 'Tax', 'r', 'w']) G = hank_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) @@ -70,7 +70,7 @@ def test_two_asset_td(two_asset_hank_dag): two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag T = 30 - household = two_asset_model._blocks_unsorted[0] + household = two_asset_model['household'] J_ha = household.jacobian(ss=ss, T=T, inputs=['N', 'r', 'ra', 'rb', 'tax', 'w']) G = two_asset_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) @@ -100,7 +100,7 @@ def test_two_asset_solved_v_simple_td(two_asset_hank_dag): targets_simple = ["asset_mkt", "fisher", "wnkpc", "nkpc", "equity", "inv", "val"] T = 30 - household = two_asset_model._blocks_unsorted[0] + household = two_asset_model['household'] J_ha = household.jacobian(ss=ss, T=T, inputs=['N', 'r', 'ra', 'rb', 'tax', 'w']) G = two_asset_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T, Js={'household': J_ha}) G_simple = two_asset_model_simple.solve_jacobian(ss, unknowns_simple, targets_simple, exogenous, T=T, diff --git a/tests/utils/test_DAG.py b/tests/utils/test_DAG.py new file mode 100644 index 0000000..8a8bbb0 --- /dev/null +++ b/tests/utils/test_DAG.py @@ -0,0 +1,41 @@ +from sequence_jacobian.utilities.graph import DAG +from sequence_jacobian.utilities.ordered_set import OrderedSet + + +class Block: + def __init__(self, inputs, outputs): + self.inputs = OrderedSet(inputs) + self.outputs = OrderedSet(outputs) + + +test_dag = DAG([Block(inputs=['a', 'b', 'z'], outputs=['c', 'd']), + Block(inputs=['a', 'e'], outputs=['b']), + Block(inputs = ['d'], outputs=['f'])]) + + +def test_dag_constructor(): + # the blocks should be ordered 1, 0, 2 + assert list(test_dag.blocks[0].inputs) == ['a', 'e'] + assert list(test_dag.blocks[1].inputs) == ['a', 'b', 'z'] + assert list(test_dag.blocks[2].inputs) == ['d'] + + assert set(test_dag.inmap['a']) == {0, 1} + assert set(test_dag.inmap['b']) == {1} + + assert test_dag.outmap['c'] == 1 + assert test_dag.outmap['f'] == 2 + assert test_dag.outmap['d'] == 1 + + assert set(test_dag.adj[0]) == {1} + assert set(test_dag.adj[1]) == {2} + assert set(test_dag.revadj[2]) == {1} + assert set(test_dag.revadj[1]) == {0} + + +def test_visited(): + test_dag.visit_from_outputs(['f']) == OrderedSet([0, 1, 2]) + test_dag.visit_from_outputs(['b']) == OrderedSet([0]) + test_dag.visit_from_outputs(['d']) == OrderedSet([0, 1]) + + test_dag.visit_from_inputs(['e']) == OrderedSet([0, 1, 2]) + test_dag.visit_from_inputs(['z']) == OrderedSet([1, 2]) diff --git a/tests/utils/test_function.py b/tests/utils/test_function.py index 510de27..0365481 100644 --- a/tests/utils/test_function.py +++ b/tests/utils/test_function.py @@ -1,5 +1,6 @@ from sequence_jacobian.utilities.ordered_set import OrderedSet -from sequence_jacobian.utilities.function import DifferentiableExtendedFunction, ExtendedFunction, ExtendedParallelFunction, metadata +from sequence_jacobian.utilities.function import (DifferentiableExtendedFunction, ExtendedFunction, + ExtendedParallelFunction, CombinedExtendedFunction, metadata) import numpy as np def f1(a, b, c): @@ -24,7 +25,7 @@ def test_extended_function(): def f3(a, b): - c = a*b - 3*a + c = a*b - 5*a d = 3*b**2 return c, d @@ -36,29 +37,32 @@ def test_differentiable_extended_function(): inputs1 = {'a': 0.5} diff = extf3.differentiable(ss1).diff(inputs1) - assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['c'], -1.5) assert np.isclose(diff['d'], 0) -def f4(a, e): - f = a / e +def f4(a, c, e): + f = a / c + a * e - c return f -def test_differentiable_extended_parallel_function(): - fs = ExtendedParallelFunction([f3, f4]) +def test_differentiable_combined_extended_function(): + # swapping in combined extended function to see if it works! + fs = CombinedExtendedFunction([f3, f4]) ss1 = {'a': 1, 'b': 2, 'e': 4} + ss1.update(fs(ss1)) + inputs1 = {'a': 0.5, 'e': 1} diff = fs.differentiable(ss1).diff(inputs1) - assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['c'], -1.5) assert np.isclose(diff['d'], 0) - assert np.isclose(diff['f'], 1/16) + assert np.isclose(diff['f'], 4.5) # test narrowing down outputs diff = fs.differentiable(ss1).diff(inputs1, outputs=['c','d']) - assert np.isclose(diff['c'], -0.5) + assert np.isclose(diff['c'], -1.5) assert np.isclose(diff['d'], 0) assert list(diff) == ['c', 'd'] @@ -66,9 +70,10 @@ def test_differentiable_extended_parallel_function(): inputs2 = {'e': -2} diff = fs.differentiable(ss1).diff2(inputs2) assert list(diff) == ['f'] - assert np.isclose(diff['f'], 1/8) + assert np.isclose(diff['f'], -2) # if we ask for output from first function but no inputs shocked, shouldn't be there! diff = fs.differentiable(ss1).diff(inputs2, outputs=['c', 'f']) assert list(diff) == ['f'] - assert np.isclose(diff['f'], 1/8) + assert np.isclose(diff['f'], -2) + From 094bbc5bcf160ef136b75dd9708e966d24b01c21 Mon Sep 17 00:00:00 2001 From: Matthew Rognlie Date: Wed, 29 Sep 2021 12:07:42 -0500 Subject: [PATCH 284/288] switched from ExtendedParallelFunction to CombinedExtendedFunction for hetinputs and hetoutputs, deleted ExtendedParallelFunction; in principle now we can have dependencies within each (should see if we need to optimize code later) --- src/sequence_jacobian/blocks/het_block.py | 14 +-- src/sequence_jacobian/utilities/function.py | 129 +++----------------- tests/utils/test_function.py | 2 +- 3 files changed, 25 insertions(+), 120 deletions(-) diff --git a/src/sequence_jacobian/blocks/het_block.py b/src/sequence_jacobian/blocks/het_block.py index a2b340d..aafeef7 100644 --- a/src/sequence_jacobian/blocks/het_block.py +++ b/src/sequence_jacobian/blocks/het_block.py @@ -5,7 +5,7 @@ from .block import Block from .. import utilities as utils from ..classes import SteadyStateDict, ImpulseDict, JacobianDict -from ..utilities.function import ExtendedFunction, ExtendedParallelFunction +from ..utilities.function import ExtendedFunction, CombinedExtendedFunction from ..utilities.ordered_set import OrderedSet from ..utilities.bijection import Bijection from .support.het_support import ForwardShockableTransition, ExpectationShockableTransition, lottery_1d, lottery_2d, Markov, CombinedTransition, Transition @@ -41,9 +41,9 @@ def __init__(self, backward_fun, exogenous, policy, backward, backward_init=None # A HetBlock can have heterogeneous inputs and heterogeneous outputs, henceforth `hetinput` and `hetoutput`. if hetinputs is not None: - hetinputs = ExtendedParallelFunction(hetinputs) + hetinputs = CombinedExtendedFunction(hetinputs) if hetoutputs is not None: - hetoutputs = ExtendedParallelFunction(hetoutputs) + hetoutputs = CombinedExtendedFunction(hetoutputs) self.process_hetinputs_hetoutputs(hetinputs, hetoutputs, tocopy=False) if len(self.policy) > 2: @@ -375,7 +375,7 @@ def backward_step_fakenews(self, din_dict, output_list, differentiable_backward_ # and also affect aggregate outcomes today if differentiable_hetoutput is not None and (output_list & differentiable_hetoutput.outputs): - shocked_outputs.update(differentiable_hetoutput.diff({**shocked_outputs, **din_dict})) + shocked_outputs.update(differentiable_hetoutput.diff({**shocked_outputs, **din_dict}, outputs=differentiable_hetoutput.outputs & output_list)) curlyY = {k: np.vdot(D, shocked_outputs[k]) for k in output_list} # add effects from perturbation to exog on beginning-of-period expectations in curlyV and curlyY @@ -413,7 +413,7 @@ def jac_backward_prelim(self, ss, h, exog, twosided): '''HetInput and HetOutput options and processing''' - def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunction], hetoutputs: Optional[ExtendedParallelFunction], tocopy=True): + def process_hetinputs_hetoutputs(self, hetinputs: Optional[CombinedExtendedFunction], hetoutputs: Optional[CombinedExtendedFunction], tocopy=True): if tocopy: self = copy.copy(self) inputs = self.original_inputs.copy() @@ -442,7 +442,7 @@ def process_hetinputs_hetoutputs(self, hetinputs: Optional[ExtendedParallelFunct def add_hetinputs(self, functions): if self.hetinputs is None: - return self.process_hetinputs_hetoutputs(ExtendedParallelFunction(functions), self.hetoutputs) + return self.process_hetinputs_hetoutputs(CombinedExtendedFunction(functions), self.hetoutputs) else: return self.process_hetinputs_hetoutputs(self.hetinputs.add(functions), self.hetoutputs) @@ -451,7 +451,7 @@ def remove_hetinputs(self, names): def add_hetoutputs(self, functions): if self.hetoutputs is None: - return self.process_hetinputs_hetoutputs(self.hetinputs, ExtendedParallelFunction(functions)) + return self.process_hetinputs_hetoutputs(self.hetinputs, CombinedExtendedFunction(functions)) else: return self.process_hetinputs_hetoutputs(self.hetinputs, self.hetoutputs.add(functions)) diff --git a/src/sequence_jacobian/utilities/function.py b/src/sequence_jacobian/utilities/function.py index 462fdc2..75512d4 100644 --- a/src/sequence_jacobian/utilities/function.py +++ b/src/sequence_jacobian/utilities/function.py @@ -146,118 +146,6 @@ def hide_zero_values(d): return {k: v for k, v in d.items() if not np.allclose(v, 0)} -class ExtendedParallelFunction(ExtendedFunction): - def __init__(self, fs, name=None): - inputs = OrderedSet([]) - outputs = OrderedSet([]) - functions = {} - for f in fs: - ext_f = ExtendedFunction(f) - if not outputs.isdisjoint(ext_f.outputs): - raise ValueError(f'Overlap in outputs of ParallelFunction: {ext_f.name} and others both have {outputs & ext_f.outputs}') - inputs |= ext_f.inputs - outputs |= ext_f.outputs - - if ext_f.name in functions: - raise ValueError(f'Overlap in function names of ParallelFunction: {ext_f.name} listed twice') - functions[ext_f.name] = ext_f - - self.inputs = inputs - self.outputs = outputs - self.functions = functions - - if name is None: - names = list(functions) - if len(names) == 1: - self.name = names[0] - else: - self.name = f'{names[0]}_{names[-1]}' - else: - self.name = name - - def __call__(self, input_dict, outputs=None): - results = {} - for f in self.functions.values(): - if outputs is None or not f.outputs.isdisjoint(outputs): - results.update(f(input_dict)) - if outputs is not None: - results = {k: results[k] for k in outputs} - return results - - def call_on_deviations(self, ss, dev_dict, outputs=None): - results = {} - input_dict = {**ss, **dev_dict} - for f in self.functions.values(): - if not f.inputs.isdisjoint(dev_dict): - if outputs is None or not f.outputs.isdisjoint(outputs): - results.update(f(input_dict)) - if outputs is not None: - results = {k: results[k] for k in outputs if k in results} - return results - - def wrapped_call(self, input_dict, preprocess=None, postprocess=None): - raise NotImplementedError - - def add(self, f): - if inspect.isfunction(f) or isinstance(f, ExtendedFunction): - return ExtendedParallelFunction(list(self.functions.values()) + [f]) - else: - # otherwise assume f is iterable - return ExtendedParallelFunction(list(self.functions.values()) + list(f)) - - def remove(self, name): - if isinstance(name, str): - return ExtendedParallelFunction([v for k, v in self.functions.items() if k != name]) - else: - # otherwise assume name is iterable - return ExtendedParallelFunction([v for k, v in self.functions.items() if k not in name]) - - def children(self): - return OrderedSet(self.functions) - - def differentiable(self, input_dict, h=1E-5, twosided=False): - return DifferentiableExtendedParallelFunction(self.functions, self.name, self.inputs, self.outputs, input_dict, h, twosided) - - -class DifferentiableExtendedParallelFunction(ExtendedParallelFunction, DifferentiableExtendedFunction): - def __init__(self, functions, name, inputs, outputs, input_dict, h=1E-5, twosided=False): - self.name, self.inputs, self.outputs = name, inputs, outputs - diff_functions = {} - for k, f in functions.items(): - diff_functions[k] = f.differentiable(input_dict, h) - self.diff_functions = diff_functions - self.default_twosided = twosided - - def diff(self, shock_dict, h=None, outputs=None, hide_zeros=False, twosided=False): - if twosided is None: - twosided = self.default_twosided - - if not twosided: - return self.diff1(shock_dict, h, outputs, hide_zeros) - else: - return self.diff2(shock_dict, h, outputs, hide_zeros) - - def diff1(self, shock_dict, h=None, outputs=None, hide_zeros=False): - results = {} - for f in self.diff_functions.values(): - if not f.inputs.isdisjoint(shock_dict): - if outputs is None or not f.outputs.isdisjoint(outputs): - results.update(f.diff1(shock_dict, h, hide_zeros)) - if outputs is not None: - results = {k: results[k] for k in outputs if k in results} - return results - - def diff2(self, shock_dict, h=None, outputs=None, hide_zeros=False): - results = {} - for f in self.diff_functions.values(): - if not f.inputs.isdisjoint(shock_dict): - if outputs is None or not f.outputs.isdisjoint(outputs): - results.update(f.diff2(shock_dict, h, hide_zeros)) - if outputs is not None: - results = {k: results[k] for k in outputs if k in results} - return results - - class CombinedExtendedFunction(ExtendedFunction): def __init__(self, fs, name=None): self.dag = graph.DAG([ExtendedFunction(f) for f in fs]) @@ -312,6 +200,23 @@ def filter(self, function_list, inputs, outputs=None): def wrapped_call(self, input_dict, preprocess=None, postprocess=None): raise NotImplementedError + def add(self, f): + if inspect.isfunction(f) or isinstance(f, ExtendedFunction): + return CombinedExtendedFunction(list(self.functions.values()) + [f]) + else: + # otherwise assume f is iterable + return CombinedExtendedFunction(list(self.functions.values()) + list(f)) + + def remove(self, name): + if isinstance(name, str): + return CombinedExtendedFunction([v for k, v in self.functions.items() if k != name]) + else: + # otherwise assume name is iterable + return CombinedExtendedFunction([v for k, v in self.functions.items() if k not in name]) + + def children(self): + return OrderedSet(self.functions) + def differentiable(self, input_dict, h=1E-5, twosided=False): return DifferentiableCombinedExtendedFunction(self.functions, self.dag, self.name, self.inputs, self.outputs, input_dict, h, twosided) diff --git a/tests/utils/test_function.py b/tests/utils/test_function.py index 0365481..b1afb58 100644 --- a/tests/utils/test_function.py +++ b/tests/utils/test_function.py @@ -1,6 +1,6 @@ from sequence_jacobian.utilities.ordered_set import OrderedSet from sequence_jacobian.utilities.function import (DifferentiableExtendedFunction, ExtendedFunction, - ExtendedParallelFunction, CombinedExtendedFunction, metadata) + CombinedExtendedFunction, metadata) import numpy as np def f1(a, b, c): From 2bd2d6b6aaaca715816008da88a3fd494f1feda1 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Wed, 29 Sep 2021 13:41:07 -0400 Subject: [PATCH 285/288] every test passes with dag of hetinputs --- src/sequence_jacobian/examples/hank.py | 5 +---- src/sequence_jacobian/examples/krusell_smith.py | 7 +++---- src/sequence_jacobian/examples/two_asset.py | 5 +---- tests/base/test_transitional_dynamics.py | 4 ++-- tests/base/test_workflow.py | 6 ++---- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/sequence_jacobian/examples/hank.py b/src/sequence_jacobian/examples/hank.py index 3085d39..1c4f629 100644 --- a/src/sequence_jacobian/examples/hank.py +++ b/src/sequence_jacobian/examples/hank.py @@ -55,8 +55,6 @@ def partial_ss_solution(B_Y, Y, Z, mu): '''Part 2: Embed HA block''' -# This cannot be a hetinput, bc `transfers` depends on it -@simple def make_grids(rho_s, sigma_s, nS, amax, nA): e_grid, pi_e, Pi = utils.discretize.markov_rouwenhorst(rho=rho_s, sigma=sigma_s, N=nS) a_grid = utils.discretize.agrid(amax=amax, n=nA) @@ -86,9 +84,8 @@ def labor_supply(n, e_grid): def dag(): # Combine blocks - household = hh.household.add_hetinputs([transfers, wages]) + household = hh.household.add_hetinputs([transfers, wages, make_grids]) household = household.add_hetoutputs([labor_supply]) - household = combine([make_grids, household], name='HH') blocks = [household, firm, monetary, fiscal, mkt_clearing, nkpc] helper_blocks = [partial_ss_solution] hank_model = create_model(blocks, name="One-Asset HANK") diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py index 78b91fa..d79e93a 100644 --- a/src/sequence_jacobian/examples/krusell_smith.py +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -32,7 +32,6 @@ def firm_ss_solution(r, Y, L, delta, alpha): '''Part 2: Embed HA block''' -@simple def make_grids(rho, sigma, nS, amax, nA): e_grid, _, Pi = utils.discretize.markov_rouwenhorst(rho=rho, sigma=sigma, N=nS) a_grid = utils.discretize.agrid(amax=amax, n=nA) @@ -48,8 +47,8 @@ def income(w, e_grid): def dag(): # Combine blocks - household = hh.household.add_hetinputs([income]) - blocks = [household, firm, make_grids, mkt_clearing] + household = hh.household.add_hetinputs([income, make_grids]) + blocks = [household, firm, mkt_clearing] helper_blocks = [firm_ss_solution] ks_model = create_model(blocks, name="Krusell-Smith") @@ -80,7 +79,7 @@ def aggregate(A_patient, A_impatient, C_patient, C_impatient, mass_patient): def remapped_dag(): # Create 2 versions of the household block using `remap` - household = hh.household.add_hetinputs([income]) + household = hh.household.add_hetinputs([income, make_grids]) to_map = ['beta', *household.outputs] hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') diff --git a/src/sequence_jacobian/examples/two_asset.py b/src/sequence_jacobian/examples/two_asset.py index 3c44a7b..de24893 100644 --- a/src/sequence_jacobian/examples/two_asset.py +++ b/src/sequence_jacobian/examples/two_asset.py @@ -139,8 +139,6 @@ def partial_ss_step2(tax, w, UCE, N, muw, frisch): '''Part 2: Embed HA block''' -# This cannot be a hetinput, bc `income` depends on it -@simple def make_grids(bmax, amax, kmax, nB, nA, nK, nZ, rho_z, sigma_z): b_grid = utils.discretize.agrid(amax=bmax, n=nB) a_grid = utils.discretize.agrid(amax=amax, n=nA) @@ -158,8 +156,7 @@ def income(e_grid, tax, w, N): def dag(): # Combine Blocks - household = hh.household.add_hetinputs([income]) - household = combine([make_grids, household], name='HH') + household = hh.household.add_hetinputs([income, make_grids]) production = combine([labor, investment]) production_solved = production.solved(unknowns={'Q': 1., 'K': 10.}, targets=['inv', 'val'], solver='broyden_custom') diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 0710ca0..4922325 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -89,8 +89,8 @@ def test_two_asset_td(two_asset_hank_dag): def test_two_asset_solved_v_simple_td(two_asset_hank_dag): two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag - household = hh.household.add_hetinputs([two_asset.income]) - blocks_simple = [household, two_asset.make_grids, two_asset.pricing, two_asset.arbitrage, + household = hh.household.add_hetinputs([two_asset.income, two_asset.make_grids]) + blocks_simple = [household, two_asset.pricing, two_asset.arbitrage, two_asset.labor, two_asset.investment, two_asset.dividend, two_asset.taylor, two_asset.fiscal, two_asset.share_value, two_asset.finance, two_asset.wage, two_asset.union, diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 63ff13f..60604c9 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -1,12 +1,11 @@ import numpy as np -from sequence_jacobian import simple, solved, combine, create_model, markov_rouwenhorst, agrid +from sequence_jacobian import simple, solved, create_model, markov_rouwenhorst, agrid from sequence_jacobian.classes.impulse_dict import ImpulseDict from sequence_jacobian.examples.hetblocks import household_sim as hh '''Part 1: Household block''' -@simple def make_grids(rho_e, sd_e, nE, amin, amax, nA): e_grid, e_dist, Pi = markov_rouwenhorst(rho=rho_e, sigma=sd_e, N=nE) a_grid = agrid(amin=amin, amax=amax, n=nA) @@ -108,9 +107,8 @@ def union_ss(atw, UCE, muw, N, nu, kappaw, beta, pi): def test_all(): # Assemble HA block (want to test nesting) - household_ha = hh.household.add_hetinputs([income]) + household_ha = hh.household.add_hetinputs([make_grids, income]) household_ha = household_ha.add_hetoutputs([mpcs, weighted_uc]).rename('household_ha') - household_ha = combine([household_ha, make_grids], name='HH') # Assemble DAG (for transition dynamics) dag = {} From 2b87bce99edaf0ae512207b95b5c513907a472f5 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 11 Oct 2021 12:22:10 -0400 Subject: [PATCH 286/288] stopped using helper blocks in examples, updated tests accordingly --- src/sequence_jacobian/examples/hank.py | 26 ++++++------- .../examples/krusell_smith.py | 39 +++++++++---------- src/sequence_jacobian/examples/two_asset.py | 33 ++++++++-------- tests/base/test_estimation.py | 4 +- tests/base/test_jacobian.py | 2 +- tests/base/test_options.py | 24 ++++++------ tests/base/test_public_classes.py | 2 +- tests/base/test_transitional_dynamics.py | 8 ++-- tests/base/test_workflow.py | 25 ++++++------ tests/robustness/test_steady_state.py | 34 +++++++--------- 10 files changed, 93 insertions(+), 104 deletions(-) diff --git a/src/sequence_jacobian/examples/hank.py b/src/sequence_jacobian/examples/hank.py index 1c4f629..da444da 100644 --- a/src/sequence_jacobian/examples/hank.py +++ b/src/sequence_jacobian/examples/hank.py @@ -2,8 +2,7 @@ from .. import utilities as utils from ..blocks.simple_block import simple -from ..blocks.solved_block import solved -from ..blocks.combined_block import create_model, combine +from ..blocks.combined_block import create_model from .hetblocks import household_labor as hh @@ -45,11 +44,10 @@ def mkt_clearing(A, NE, C, L, Y, B, pi, mu, kappa): @simple -def partial_ss_solution(B_Y, Y, Z, mu): +def nkpc_ss(Z, mu): '''Solve (w) to hit targets for (nkpc_res)''' w = Z / mu - B = B_Y * Y - return w, B + return w '''Part 2: Embed HA block''' @@ -87,23 +85,23 @@ def dag(): household = hh.household.add_hetinputs([transfers, wages, make_grids]) household = household.add_hetoutputs([labor_supply]) blocks = [household, firm, monetary, fiscal, mkt_clearing, nkpc] - helper_blocks = [partial_ss_solution] + blocks_ss = [household, firm, monetary, fiscal, mkt_clearing, nkpc_ss] hank_model = create_model(blocks, name="One-Asset HANK") + hank_model_ss = create_model(blocks_ss, name="One-Asset HANK") # Steady state - calibration = {'r': 0.005, 'rstar': 0.005, 'eis': 0.5, 'frisch': 0.5, 'B_Y': 5.6, + calibration = {'r': 0.005, 'rstar': 0.005, 'eis': 0.5, 'frisch': 0.5, 'B': 5.6, 'mu': 1.2, 'rho_s': 0.966, 'sigma_s': 0.5, 'kappa': 0.1, 'phi': 1.5, 'Y': 1., 'Z': 1., 'pi': 0., 'nS': 2, 'amax': 150, 'nA': 10} - unknowns_ss = {'beta': 0.986, 'vphi': 0.8, 'w': 0.8} - targets_ss = {'asset_mkt': 0., 'NE': 1., 'nkpc_res': 0.} - ss = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - solver='broyden_custom', - helper_blocks=helper_blocks, - helper_targets=['nkpc_res']) + unknowns_ss = {'beta': 0.986, 'vphi': 0.8} + targets_ss = {'asset_mkt': 0., 'NE': 1.} + cali = hank_model_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, + solver='broyden_custom') + ss = hank_model.steady_state(cali) # Transitional dynamics unknowns = ['w', 'Y', 'pi'] targets = ['asset_mkt', 'goods_mkt', 'nkpc_res'] exogenous = ['rstar', 'Z'] - return hank_model, ss, unknowns, targets, exogenous + return hank_model_ss, ss, hank_model, unknowns, targets, exogenous diff --git a/src/sequence_jacobian/examples/krusell_smith.py b/src/sequence_jacobian/examples/krusell_smith.py index d79e93a..ff06a4e 100644 --- a/src/sequence_jacobian/examples/krusell_smith.py +++ b/src/sequence_jacobian/examples/krusell_smith.py @@ -1,6 +1,6 @@ from .. import utilities as utils from ..blocks.simple_block import simple -from ..blocks.combined_block import create_model, combine +from ..blocks.combined_block import create_model from .hetblocks import household_sim as hh @@ -22,12 +22,13 @@ def mkt_clearing(K, A, Y, C, delta): @simple -def firm_ss_solution(r, Y, L, delta, alpha): +def firm_ss(r, Y, L, delta, alpha): '''Solve for (Z, K) given targets for (Y, r).''' rk = r + delta K = alpha * Y / rk Z = Y / K ** alpha / L ** (1 - alpha) - return K, Z + w = (1 - alpha) * Z * (K / L) ** alpha + return K, Z, w '''Part 2: Embed HA block''' @@ -48,24 +49,22 @@ def income(w, e_grid): def dag(): # Combine blocks household = hh.household.add_hetinputs([income, make_grids]) - blocks = [household, firm, mkt_clearing] - helper_blocks = [firm_ss_solution] - ks_model = create_model(blocks, name="Krusell-Smith") + ks_model = create_model([household, firm, mkt_clearing], name="Krusell-Smith") + ks_model_ss = create_model([household, firm_ss, mkt_clearing], name="Krusell-Smith SS") # Steady state - calibration = {'eis': 1, 'delta': 0.025, 'alpha': 0.11, 'rho': 0.966, 'sigma': 0.5, - 'L': 1.0, 'nS': 2, 'nA': 10, 'amax': 200, 'r': 0.01} - unknowns_ss = {'beta': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.85, 'K': 3.} - targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} - ss = ks_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', - helper_blocks=helper_blocks, helper_targets=['Y', 'r']) + calibration = {'eis': 1.0, 'delta': 0.025, 'alpha': 0.11, 'rho': 0.966, 'sigma': 0.5, + 'Y': 1.0, 'L': 1.0, 'nS': 2, 'nA': 10, 'amax': 200, 'r': 0.01} + unknowns_ss = {'beta': (0.98 / 1.01, 0.999 / 1.01)} + targets_ss = {'asset_mkt': 0.} + ss = ks_model_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq') # Transitional dynamics inputs = ['Z'] unknowns = ['K'] targets = ['asset_mkt'] - return ks_model, ss, unknowns, targets, inputs + return ks_model_ss, ss, ks_model, unknowns, targets, inputs '''Part 3: Permanent beta heterogeneity''' @@ -84,20 +83,20 @@ def remapped_dag(): hh_patient = household.remap({k: k + '_patient' for k in to_map}).rename('hh_patient') hh_impatient = household.remap({k: k + '_impatient' for k in to_map}).rename('hh_impatient') blocks = [hh_patient, hh_impatient, firm, mkt_clearing, aggregate] + blocks_ss = [hh_patient, hh_impatient, firm_ss, mkt_clearing, aggregate] ks_remapped = create_model(blocks, name='KS-beta-het') + ks_remapped_ss = create_model(blocks_ss, name='KS-beta-het') # Steady State - calibration = {'eis': 1., 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'L': 1.0, + calibration = {'eis': 1., 'delta': 0.025, 'alpha': 0.3, 'rho': 0.966, 'sigma': 0.5, 'Y': 1.0, 'L': 1.0, 'nS': 3, 'nA': 100, 'amax': 1000, 'beta_impatient': 0.985, 'mass_patient': 0.5} - unknowns_ss = {'beta_patient': (0.98 / 1.01, 0.999 / 1.01), 'Z': 0.5, 'K': 8.} - targets_ss = {'asset_mkt': 0., 'Y': 1., 'r': 0.01} - helper_blocks = [firm_ss_solution] - ss = ks_remapped.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq', - helper_blocks=helper_blocks, helper_targets=['Y', 'r']) + unknowns_ss = {'beta_patient': (0.98 / 1.01, 0.999 / 1.01)} + targets_ss = {'asset_mkt': 0.} + ss = ks_remapped_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='brentq') # Transitional Dynamics/Jacobian Calculation unknowns = ['K'] targets = ['asset_mkt'] exogenous = ['Z'] - return ks_remapped, ss, unknowns, targets, ss, exogenous + return ks_remapped_ss, ss, ks_remapped, unknowns, targets, ss, exogenous diff --git a/src/sequence_jacobian/examples/two_asset.py b/src/sequence_jacobian/examples/two_asset.py index de24893..a3de24b 100644 --- a/src/sequence_jacobian/examples/two_asset.py +++ b/src/sequence_jacobian/examples/two_asset.py @@ -108,13 +108,12 @@ def arbitrage_solved(div, p, r): @simple -def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): +def partial_ss(Y, N, K, r, tot_wealth, Bg, delta): """Solves for (mup, alpha, Z, w) to hit (tot_wealth, N, K, pi).""" # 1. Solve for markup to hit total wealth p = tot_wealth - Bg mc = 1 - r * (p - K) / Y mup = 1 / mc - wealth = tot_wealth # 2. Solve for capital share to hit K alpha = (r + delta) * K / Y / mc @@ -124,13 +123,12 @@ def partial_ss_step1(Y, N, K, r, tot_wealth, Bg, delta): # 4. Solve for w such that piw = 0 w = mc * (1 - alpha) * Y / N - piw = 0 - return p, mc, mup, wealth, alpha, Z, w, piw + return p, mc, mup, alpha, Z, w @simple -def partial_ss_step2(tax, w, UCE, N, muw, frisch): +def union_ss(tax, w, UCE, N, muw, frisch): """Solves for (vphi) to hit (wnkpc).""" vphi = (1 - tax) * w * UCE / muw / N ** (1 + 1 / frisch) wnkpc = vphi * N ** (1 + 1 / frisch) - (1 - tax) * w * UCE / muw @@ -162,27 +160,28 @@ def dag(): targets=['inv', 'val'], solver='broyden_custom') blocks = [household, pricing_solved, arbitrage_solved, production_solved, dividend, taylor, fiscal, share_value, finance, wage, union, mkt_clearing] - helper_blocks = [partial_ss_step1, partial_ss_step2] two_asset_model = create_model(blocks, name='Two-Asset HANK') + # Steadt state DAG + blocks_ss = [household, partial_ss, + dividend, taylor, fiscal, share_value, finance, union_ss, mkt_clearing] + two_asset_model_ss = create_model(blocks_ss, name='Two-Asset HANK SS') + # Steady State - calibration = {'Y': 1., 'r': 0.0125, 'rstar': 0.0125, 'tot_wealth': 14, 'delta': 0.02, + calibration = {'Y': 1., 'N': 1.0, 'K': 10., 'r': 0.0125, 'rstar': 0.0125, 'tot_wealth': 14, + 'delta': 0.02, 'pi': 0., 'kappap': 0.1, 'muw': 1.1, 'Bh': 1.04, 'Bg': 2.8, 'G': 0.2, 'eis': 0.5, 'frisch': 1, 'chi0': 0.25, 'chi2': 2, 'epsI': 4, 'omega': 0.005, 'kappaw': 0.1, 'phi': 1.5, 'nZ': 3, 'nB': 10, 'nA': 16, 'nK': 4, 'bmax': 50, 'amax': 4000, 'kmax': 1, 'rho_z': 0.966, 'sigma_z': 0.92} - unknowns_ss = {'beta': 0.976, 'chi1': 6.5, 'vphi': 1.71, 'Z': 0.4678, - 'alpha': 0.3299, 'mup': 1.015, 'w': 0.66} - targets_ss = {'asset_mkt': 0., 'B': 'Bh', 'wnkpc': 0., 'piw': 0.0, 'K': 10., - 'wealth': 'tot_wealth', 'N': 1.0} - ss = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - solver='broyden_custom', - helper_blocks=helper_blocks, - helper_targets=['wnkpc', 'piw', 'K', 'wealth', 'N']) - + unknowns_ss = {'beta': 0.976, 'chi1': 6.5} + targets_ss = {'asset_mkt': 0., 'B': 'Bh'} + cali = two_asset_model_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, solver='broyden_custom') + ss = two_asset_model.steady_state(cali) + # Transitional Dynamics/Jacobian Calculation unknowns = ['r', 'w', 'Y'] targets = ['asset_mkt', 'fisher', 'wnkpc'] exogenous = ['rstar', 'Z', 'G'] - return two_asset_model, ss, unknowns, targets, exogenous + return two_asset_model_ss, ss, two_asset_model, unknowns, targets, exogenous diff --git a/tests/base/test_estimation.py b/tests/base/test_estimation.py index 6bf589c..841c283 100644 --- a/tests/base/test_estimation.py +++ b/tests/base/test_estimation.py @@ -1,5 +1,5 @@ """Test all models' estimation calculations""" - +'' import pytest import numpy as np @@ -9,7 +9,7 @@ # See test_determinacy.py for the to-do describing this suppression @pytest.mark.filterwarnings("ignore:.*cannot be safely interpreted as an integer.*:DeprecationWarning") def test_krusell_smith_estimation(krusell_smith_dag): - ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag + _, ss, ks_model, unknowns, targets, exogenous = krusell_smith_dag np.random.seed(41234) T = 50 diff --git a/tests/base/test_jacobian.py b/tests/base/test_jacobian.py index 6d91073..4dee686 100644 --- a/tests/base/test_jacobian.py +++ b/tests/base/test_jacobian.py @@ -3,7 +3,7 @@ import numpy as np def test_ks_jac(krusell_smith_dag): - ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag + _, ss, ks_model, unknowns, targets, exogenous = krusell_smith_dag household, firm = ks_model['household'], ks_model['firm'] T = 10 diff --git a/tests/base/test_options.py b/tests/base/test_options.py index 67456d3..7989d86 100644 --- a/tests/base/test_options.py +++ b/tests/base/test_options.py @@ -3,7 +3,7 @@ from sequence_jacobian.examples import krusell_smith def test_jacobian_h(krusell_smith_dag): - dag, ss, *_ = krusell_smith_dag + _, ss, dag, *_ = krusell_smith_dag hh = dag['household'] lowacc = hh.jacobian(ss, inputs=['r'], outputs=['C'], T=10, h=0.05) @@ -25,7 +25,7 @@ def test_jacobian_h(krusell_smith_dag): def test_jacobian_steady_state(krusell_smith_dag): - dag = krusell_smith_dag[0] + dag = krusell_smith_dag[2] calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01, 'beta': 0.96, "Z": 0.85, "K": 3.} @@ -38,15 +38,13 @@ def test_jacobian_steady_state(krusell_smith_dag): def test_steady_state_solution(krusell_smith_dag): - dag, *_ = krusell_smith_dag - helper_blocks = [krusell_smith.firm_ss_solution] + dag_ss, ss, *_ = krusell_smith_dag - calibration = {"eis": 1, "delta": 0.025, "alpha": 0.11, "rho": 0.966, "sigma": 0.5, - "L": 1.0, "nS": 2, "nA": 10, "amax": 200, "r": 0.01} - unknowns_ss = {"beta": (0.98 / 1.01, 0.999 / 1.01), "Z": 0.85, "K": 3.} - targets_ss = {"asset_mkt": 0., "Y": 1., "r": 0.01} - - pytest.raises(RuntimeError, dag.solve_steady_state, calibration, - unknowns_ss, targets_ss, solver="brentq", - helper_blocks=helper_blocks, helper_targets=["Y", "r"], - ttol=1E-2, ctol=1E-9) + calibration = {'eis': 1.0, 'delta': 0.025, 'alpha': 0.11, 'rho': 0.966, 'sigma': 0.5, + 'Y': 1.0, 'L': 1.0, 'nS': 2, 'nA': 10, 'amax': 200, 'r': 0.01} + unknowns_ss = {'beta': (0.98 / 1.01, 0.999 / 1.01)} + targets_ss = {'asset_mkt': 0.} + + ss2 = dag_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="brentq", + ttol=1E-2, ctol=1E-2) + assert not np.isclose(ss['asset_mkt'], ss2['asset_mkt']) diff --git a/tests/base/test_public_classes.py b/tests/base/test_public_classes.py index d3509e7..55aeac3 100644 --- a/tests/base/test_public_classes.py +++ b/tests/base/test_public_classes.py @@ -9,7 +9,7 @@ from sequence_jacobian.utilities.bijection import Bijection def test_impulsedict(krusell_smith_dag): - ks_model, ss, unknowns, targets, _ = krusell_smith_dag + _, ss, ks_model, unknowns, targets, _ = krusell_smith_dag T = 200 # Linearized impulse responses as deviations diff --git a/tests/base/test_transitional_dynamics.py b/tests/base/test_transitional_dynamics.py index 4922325..c7439c1 100644 --- a/tests/base/test_transitional_dynamics.py +++ b/tests/base/test_transitional_dynamics.py @@ -31,7 +31,7 @@ def test_rbc_td(rbc_dag): def test_ks_td(krusell_smith_dag): - ks_model, ss, unknowns, targets, exogenous = krusell_smith_dag + _, ss, ks_model, unknowns, targets, exogenous = krusell_smith_dag T = 30 G = ks_model.solve_jacobian(ss, unknowns, targets, exogenous, T=T) @@ -47,7 +47,7 @@ def test_ks_td(krusell_smith_dag): def test_hank_td(one_asset_hank_dag): - hank_model, ss, unknowns, targets, exogenous = one_asset_hank_dag + _, ss, hank_model, unknowns, targets, exogenous = one_asset_hank_dag T = 30 household = hank_model['household'] @@ -67,7 +67,7 @@ def test_hank_td(one_asset_hank_dag): # TODO: needs to compute Jacobian of hetoutput `Chi` def test_two_asset_td(two_asset_hank_dag): - two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag + _, ss, two_asset_model, unknowns, targets, exogenous = two_asset_hank_dag T = 30 household = two_asset_model['household'] @@ -87,7 +87,7 @@ def test_two_asset_td(two_asset_hank_dag): def test_two_asset_solved_v_simple_td(two_asset_hank_dag): - two_asset_model, ss, unknowns, targets, exogenous = two_asset_hank_dag + _, ss, two_asset_model, unknowns, targets, exogenous = two_asset_hank_dag household = hh.household.add_hetinputs([two_asset.income, two_asset.make_grids]) blocks_simple = [household, two_asset.pricing, two_asset.arbitrage, diff --git a/tests/base/test_workflow.py b/tests/base/test_workflow.py index 60604c9..fd042bb 100644 --- a/tests/base/test_workflow.py +++ b/tests/base/test_workflow.py @@ -124,20 +124,18 @@ def test_all(): 'nA': 100, 'kappaw': 0.1, 'muw': 1.2, 'transfer': 0.143, 'rho_B': 0.9} ss = {} - ss['ha'] = dag['ha'].solve_steady_state(calibration, dissolve=['fiscal'], solver='hybr', - unknowns={'beta': 0.96, 'B': 3.0, 'vphi': 1.0, 'G': 0.2}, - targets={'asset_mkt': 0.0, 'MPC': 0.25, 'wnkpc': 0.0, 'tau': 0.334}, - helper_blocks=[union_ss], helper_targets=['wnkpc']) - - # TODO: why doesn't this work? - # ss['ra'] = dag['ra'].solve_steady_state(ss['ha'], - # unknowns={'beta': 0.96, 'vphi': 1.0}, targets={'asset_mkt': 0.0, 'wnkpc': 0.0}, - # helper_blocks=[household_ra_ss, union_ss], helper_targets=['asset_mkt', 'wnkpc'], - # dissolve=['fiscal', 'household_ra'], solver='solved') - # Constructing ss-dag manually works just fine - dag_ss = create_model([household_ra_ss, union_ss, firm, fiscal, mkt_clearing]) - ss['ra'] = dag_ss.steady_state(ss['ha'], dissolve=['fiscal']) + dag_ss = {} + dag_ss['ha'] = create_model([household_ha, union_ss, firm, fiscal, mkt_clearing]) + ss['ha'] = dag_ss['ha'].solve_steady_state(calibration, dissolve=['fiscal'], solver='hybr', + unknowns={'beta': 0.96, 'B': 3.0, 'G': 0.2}, + targets={'asset_mkt': 0.0, 'MPC': 0.25, 'tau': 0.334}) + assert np.isclose(ss['ha']['goods_mkt'], 0.0) + assert np.isclose(ss['ha']['asset_mkt'], 0.0) + assert np.isclose(ss['ha']['wnkpc'], 0.0) + + dag_ss['ra'] = create_model([household_ra_ss, union_ss, firm, fiscal, mkt_clearing]) + ss['ra'] = dag_ss['ra'].steady_state(ss['ha'], dissolve=['fiscal']) assert np.isclose(ss['ra']['goods_mkt'], 0.0) assert np.isclose(ss['ra']['asset_mkt'], 0.0) assert np.isclose(ss['ra']['wnkpc'], 0.0) @@ -165,3 +163,4 @@ def test_all(): td_nonlin_lvl = td_nonlin + ss['ha'] td_A = np.sum(td_nonlin_lvl.internals['household_ha']['a'] * td_nonlin_lvl.internals['household_ha']['D'], axis=(1, 2)) assert np.allclose(td_A - ss['ha']['A'], td_nonlin['A']) + \ No newline at end of file diff --git a/tests/robustness/test_steady_state.py b/tests/robustness/test_steady_state.py index d9c113d..b2b5d85 100644 --- a/tests/robustness/test_steady_state.py +++ b/tests/robustness/test_steady_state.py @@ -3,23 +3,20 @@ import pytest import numpy as np -from sequence_jacobian.examples import hank, two_asset - # Filter out warnings when the solver is trying to search in bad regions @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): - hank_model, ss, *_ = one_asset_hank_dag - helper_blocks = [hank.partial_ss_solution] + dag_ss, ss, dag, *_ = one_asset_hank_dag - calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B_Y": 5.6, "mu": 1.2, + calibration = {"r": 0.005, "rstar": 0.005, "eis": 0.5, "frisch": 0.5, "B": 5.6, "mu": 1.2, "rho_s": 0.966, "sigma_s": 0.5, "kappa": 0.1, "phi": 1.5, "Y": 1, "Z": 1, "L": 1, "pi": 0, "nS": 2, "amax": 150, "nA": 10} - unknowns_ss = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10.), "w": 0.8} - targets_ss = {"asset_mkt": 0, "labor_mkt": 0, "nkpc_res": 0.} - ss_ref = hank_model.solve_steady_state(calibration, unknowns_ss, targets_ss, - helper_blocks=helper_blocks, helper_targets=["nkpc_res"], solver="hybr", - constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) + unknowns_ss = {"beta": (0.95, 0.97, 0.999 / (1 + 0.005)), "vphi": (0.001, 1.0, 10.)} + targets_ss = {"asset_mkt": 0, "labor_mkt": 0} + cali = dag_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="hybr", + constrained_kwargs={"boundary_epsilon": 5e-3, "penalty_scale": 100}) + ss_ref = dag.steady_state(cali) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) @@ -27,19 +24,18 @@ def test_hank_steady_state_w_bad_init_guesses_and_bounds(one_asset_hank_dag): @pytest.mark.filterwarnings("ignore:.*invalid value encountered in.*:RuntimeWarning") def test_two_asset_steady_state_w_bad_init_guesses_and_bounds(two_asset_hank_dag): - two_asset_model, ss, *_ = two_asset_hank_dag - helper_blocks = [two_asset.partial_ss_step1, two_asset.partial_ss_step2] + dag_ss, ss, dag, *_ = two_asset_hank_dag # Steady State - calibration = {"Y": 1., "r": 0.0125, "rstar": 0.0125, "tot_wealth": 14, "delta": 0.02, "kappap": 0.1, "muw": 1.1, + calibration = {"Y": 1., "r": 0.0125, "rstar": 0.0125, "tot_wealth": 14, "delta": 0.02, + "kappap": 0.1, "muw": 1.1, 'N': 1.0, 'K': 10., 'pi': 0.0, "Bh": 1.04, "Bg": 2.8, "G": 0.2, "eis": 0.5, "frisch": 1, "chi0": 0.25, "chi2": 2, "epsI": 4, "omega": 0.005, "kappaw": 0.1, "phi": 1.5, "nZ": 3, "nB": 10, "nA": 16, "nK": 4, "bmax": 50, "amax": 4000, "kmax": 1, "rho_z": 0.966, "sigma_z": 0.92} - unknowns_ss = {"beta": 0.976, "chi1": 6.5, "vphi": 1.71, "Z": 0.4678, "alpha": 0.3299, "mup": 1.015, 'w': 0.66} - targets_ss = {"asset_mkt": 0., "B": "Bh", 'wnkpc': 0., 'piw': 0.0, "K": 10., "wealth": "tot_wealth", "N": 1.0} - ss_ref = two_asset_model.solve_steady_state(calibration, unknowns_ss, targets_ss, solver="broyden_custom", - helper_blocks=helper_blocks, - helper_targets={'wnkpc': 0., 'piw': 0.0, "K": 10., - "wealth": "tot_wealth", "N": 1.0}) + unknowns_ss = {"beta": 0.976, "chi1": 6.5} + targets_ss = {"asset_mkt": 0., "B": "Bh"} + cali = dag_ss.solve_steady_state(calibration, unknowns_ss, targets_ss, + solver="broyden_custom") + ss_ref = dag.steady_state(cali) for k in ss.keys(): assert np.all(np.isclose(ss[k], ss_ref[k])) From a4cf7e3c1cc390141ece0f9b0fe575089a9390ff Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 11 Oct 2021 12:43:23 -0400 Subject: [PATCH 287/288] removed helper block functionality --- .../auxiliary_blocks/calibration_block.py | 46 ----------- src/sequence_jacobian/blocks/block.py | 76 +++++++------------ .../blocks/support/steady_state.py | 49 ------------ 3 files changed, 27 insertions(+), 144 deletions(-) delete mode 100644 src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py diff --git a/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py b/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py deleted file mode 100644 index 3cff60e..0000000 --- a/src/sequence_jacobian/blocks/auxiliary_blocks/calibration_block.py +++ /dev/null @@ -1,46 +0,0 @@ -"""A CombinedBlock sub-class specifically for steady state calibration with helper blocks""" - -from ..combined_block import CombinedBlock -from ...utilities.ordered_set import OrderedSet -from ...utilities.graph import block_sort_w_helpers, find_intermediate_inputs_w_helpers - - -class CalibrationBlock(CombinedBlock): - """A CalibrationBlock is a Block object, which includes a set of 'helper' blocks to be used for altering - the behavior of .steady_state and .solve_steady_state methods. In practice, the common use-case for an - CalibrationBlock is to help .solve_steady_state solve for a subset of the unknowns/targets analytically.""" - i_am_calibration_block = True - - def __init__(self, blocks, helper_blocks, calibration, name=""): - sorted_indices, inputs, outputs = block_sort_w_helpers(blocks, helper_blocks, calibration, return_io=True) - intermediate_inputs = find_intermediate_inputs_w_helpers(blocks, helper_blocks=helper_blocks) - - super().__init__(blocks, name=name, sorted_indices=sorted_indices, intermediate_inputs=intermediate_inputs) - - self.helper_blocks = helper_blocks - self.inputs, self.outputs = OrderedSet(inputs), OrderedSet(outputs) - - self.outputs_orig = set().union(*[block.outputs for block in self.blocks if block not in helper_blocks]) - self.inputs_orig = set().union(*[block.inputs for block in self.blocks if block not in helper_blocks]) - self.outputs_orig - - def __repr__(self): - return f"" - - def _steady_state(self, calibration, dissolve, helper_targets, evaluate_helpers, **block_kwargs): - """Evaluate a partial equilibrium steady state of the RedirectedBlock given a `calibration`""" - ss = calibration.copy() - helper_outputs = {} - for block in self.blocks: - if not evaluate_helpers and block in self.helper_blocks: - continue - # TODO: make this inner_dissolve better, clumsy way to dispatch dissolve only to correct children - inner_dissolve = [k for k in dissolve if self.descendants[k] == block.name] - outputs = block.steady_state(ss, dissolve=inner_dissolve, **block_kwargs) - if evaluate_helpers and block in self.helper_blocks: - helper_outputs.update({k: v for k, v in outputs.toplevel.items() if k in block.outputs | set(helper_targets.keys())}) - ss.update(outputs) - else: - # Don't overwrite entries in ss_values corresponding to what has already - # been solved for in helper_blocks so we can check for consistency after-the-fact - ss.update(outputs) if evaluate_helpers else ss.update(outputs.difference(helper_outputs)) - return ss diff --git a/src/sequence_jacobian/blocks/block.py b/src/sequence_jacobian/blocks/block.py index 9d19308..2de721d 100644 --- a/src/sequence_jacobian/blocks/block.py +++ b/src/sequence_jacobian/blocks/block.py @@ -34,15 +34,9 @@ def outputs(self): pass def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], - dissolve: List[str] = [], evaluate_helpers: bool = False, - helper_targets: dict = {}, options: Dict[str, dict] = {}, **kwargs) -> SteadyStateDict: + dissolve: List[str] = [], options: Dict[str, dict] = {}, **kwargs) -> SteadyStateDict: """Evaluate a partial equilibrium steady state of Block given a `calibration`.""" - # Special handling: 1) Find inputs/outputs of the Block w/o helpers blocks - # 2) Add all unknowns of dissolved blocks to inputs - if not evaluate_helpers: - inputs = self.inputs_orig.copy() if hasattr(self, "inputs_orig") else self.inputs.copy() - else: - inputs = self.inputs.copy() + inputs = self.inputs.copy() if isinstance(self, Parent): for k in dissolve: inputs |= self.get_attribute(k, 'unknowns').keys() @@ -50,22 +44,22 @@ def steady_state(self, calibration: Union[SteadyStateDict, UserProvidedSS], calibration = SteadyStateDict(calibration)[inputs] own_options = self.get_options(options, kwargs, 'steady_state') if isinstance(self, Parent): - if hasattr(self, 'i_am_calibration_block'): - own_options['evaluate_helpers'] = evaluate_helpers - own_options['helper_targets'] = helper_targets - return self.M @ self._steady_state(self.M.inv @ calibration, dissolve=dissolve, options=options, **own_options) + return self.M @ self._steady_state(self.M.inv @ calibration, dissolve=dissolve, + options=options, **own_options) else: return self.M @ self._steady_state(self.M.inv @ calibration, **own_options) def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], - outputs: Optional[List[str]] = None, internals: Union[Dict[str, List[str]], List[str]] = {}, + outputs: Optional[List[str]] = None, + internals: Union[Dict[str, List[str]], List[str]] = {}, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, ss_initial: Optional[SteadyStateDict] = None, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, non-linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'impulse_nonlinear') inputs = ImpulseDict(inputs) - actual_outputs, inputs_as_outputs = self.process_outputs(ss, self.make_ordered_set(inputs), self.make_ordered_set(outputs)) + actual_outputs, inputs_as_outputs = self.process_outputs(ss, + self.make_ordered_set(inputs), self.make_ordered_set(outputs)) if isinstance(self, Parent): # SolvedBlocks may use Js and may be nested in a CombinedBlock, so we need to pass them down to any parent @@ -77,8 +71,9 @@ def impulse_nonlinear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], return inputs[inputs_as_outputs] | out - def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], outputs: Optional[List[str]] = None, - Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: + def impulse_linear(self, ss: SteadyStateDict, inputs: Union[Dict[str, Array], ImpulseDict], + outputs: Optional[List[str]] = None, Js: Dict[str, JacobianDict] = {}, + options: Dict[str, dict] = {}, **kwargs) -> ImpulseDict: """Calculate a partial equilibrium, linear impulse response of `outputs` to a set of shocks in `inputs` around a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'impulse_linear') @@ -117,13 +112,14 @@ def partial_jacobians(self, ss: SteadyStateDict, inputs: Optional[List[str]] = N partial[self.name] = self.M @ partial[self.name] return partial - def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[List[str]] = None, - T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: + def jacobian(self, ss: SteadyStateDict, inputs: List[str], + outputs: Optional[List[str]] = None, + T: Optional[int] = None, Js: Dict[str, JacobianDict] = {}, + options: Dict[str, dict] = {}, **kwargs) -> JacobianDict: """Calculate a partial equilibrium Jacobian to a set of `input` shocks at a steady state `ss`.""" own_options = self.get_options(options, kwargs, 'jacobian') inputs = self.make_ordered_set(inputs) outputs, _ = self.process_outputs(ss, {}, self.make_ordered_set(outputs)) - #outputs = self.make_ordered_set(outputs) if outputs is not None else self.outputs # if you have a J for this block that has everything you need, use it if (self.name in Js) and isinstance(Js[self.name], JacobianDict) and (inputs <= Js[self.name].inputs) and (outputs <= Js[self.name].outputs): @@ -139,50 +135,32 @@ def jacobian(self, ss: SteadyStateDict, inputs: List[str], outputs: Optional[Lis Js[self.name] = self.M.inv @ Js[self.name] return self.M @ self._jacobian(self.M.inv @ ss, self.M.inv @ inputs, self.M.inv @ outputs, T=T, Js=Js, options=options, **own_options) - solve_steady_state_options = dict(solver="", solver_kwargs={}, ttol=1e-12, ctol=1e-9, verbose=False, - check_consistency=True, constrained_method="linear_continuation", constrained_kwargs={}) + solve_steady_state_options = dict(solver="", solver_kwargs={}, ttol=1e-12, ctol=1e-9, + verbose=False, constrained_method="linear_continuation", constrained_kwargs={}) def solve_steady_state(self, calibration: Dict[str, Union[Real, Array]], unknowns: Dict[str, Union[Real, Tuple[Real, Real]]], - targets: Union[Array, Dict[str, Union[str, Real]]], dissolve: List = [], - helper_blocks: List = [], helper_targets: Dict = {}, - options: Dict[str, dict] = {}, **kwargs): + targets: Union[Array, Dict[str, Union[str, Real]]], + dissolve: List = [], options: Dict[str, dict] = {}, **kwargs): """Evaluate a general equilibrium steady state of Block given a `calibration` and a set of `unknowns` and `targets` corresponding to the endogenous variables to be solved for and the target conditions that must hold in general equilibrium""" opts = self.get_options(options, kwargs, 'solve_steady_state') - if helper_blocks: - if not helper_targets: - raise ValueError("Must provide the dict of targets and their values that the `helper_blocks` solve" - " in the `helper_targets` keyword argument.") - else: - from .support.steady_state import augment_dag_w_helper_blocks - dag, ss, unknowns_to_solve, targets_to_solve = augment_dag_w_helper_blocks(self, calibration, unknowns, - targets, helper_blocks, - helper_targets) - else: - dag, ss, unknowns_to_solve, targets_to_solve = self, SteadyStateDict(calibration), unknowns, targets + ss = SteadyStateDict(calibration) solver = opts['solver'] if opts['solver'] else provide_solver_default(unknowns) - def residual(unknown_values, unknowns_keys=unknowns_to_solve.keys(), targets=targets_to_solve, - evaluate_helpers=True): + def residual(unknown_values, unknowns_keys=unknowns.keys(), targets=targets): ss.update(misc.smart_zip(unknowns_keys, unknown_values)) - ss.update(dag.steady_state(ss, dissolve=dissolve, options=options, evaluate_helpers=evaluate_helpers, **kwargs)) + ss.update(self.steady_state(ss, dissolve=dissolve, options=options, **kwargs)) return compute_target_values(targets, ss) - unknowns_solved = solve_for_unknowns(residual, unknowns_to_solve, solver, opts['solver_kwargs'], tol=opts['ttol'], verbose=opts['verbose'], - constrained_method=opts['constrained_method'], constrained_kwargs=opts['constrained_kwargs']) - - if helper_blocks and helper_targets and opts['check_consistency']: - # Add in the unknowns solved analytically by helper blocks and re-evaluate the DAG without helpers - unknowns_solved.update({k: ss[k] for k in unknowns if k not in unknowns_solved}) - cresid = np.max(abs(residual(unknowns_solved.values(), unknowns_keys=unknowns_solved.keys(), - targets=targets, evaluate_helpers=False))) - if cresid > opts['ctol']: - raise RuntimeError(f"Target value residual {cresid} exceeds ctol specified for checking" - f" the consistency of the DAG without redirection.") + _ = solve_for_unknowns(residual, unknowns, solver, opts['solver_kwargs'], + tol=opts['ttol'], verbose=opts['verbose'], + constrained_method=opts['constrained_method'], + constrained_kwargs=opts['constrained_kwargs']) + return ss solve_impulse_nonlinear_options = dict(tol=1E-8, maxit=30, verbose=True) diff --git a/src/sequence_jacobian/blocks/support/steady_state.py b/src/sequence_jacobian/blocks/support/steady_state.py index 0ad7c6b..a9464f0 100644 --- a/src/sequence_jacobian/blocks/support/steady_state.py +++ b/src/sequence_jacobian/blocks/support/steady_state.py @@ -6,32 +6,9 @@ from numbers import Real from functools import partial -from ...classes.steady_state_dict import SteadyStateDict from ...utilities import misc, solvers - -def augment_dag_w_helper_blocks(dag, calibration, unknowns, targets, helper_blocks, helper_targets): - """For a given DAG (either an individual Block or CombinedBlock), add a set of helper blocks, which help - to solve the provided set of helper targets analytically, reducing the number of unknowns/targets that need - to be solved for numerically. - """ - from ..auxiliary_blocks.calibration_block import CalibrationBlock - - targets = {t: 0. for t in targets} if isinstance(targets, list) else targets - helper_targets = {t: targets[t] for t in helper_targets} if isinstance(helper_targets, list) else helper_targets - helper_unknowns = subset_helper_block_unknowns(unknowns, helper_blocks, helper_targets) - - unknowns_to_solve = misc.dict_diff(unknowns, helper_unknowns) - targets_to_solve = misc.dict_diff(targets, helper_targets) - - ss = SteadyStateDict({**calibration, **helper_targets}) - blocks = dag.blocks if hasattr(dag, "blocks") else [dag] - dag_augmented = CalibrationBlock(blocks + helper_blocks, helper_blocks=helper_blocks, calibration=ss) - - return dag_augmented, ss, unknowns_to_solve, targets_to_solve - - def instantiate_steady_state_mutable_kwargs(dissolve, block_kwargs, solver_kwargs, constrained_kwargs): """Instantiate mutable types from `None` default values in the steady_state function""" if dissolve is None: @@ -256,32 +233,6 @@ def solve_for_unknowns(residual, unknowns, solver, solver_kwargs, residual_kwarg return dict(misc.smart_zip(unknowns.keys(), unknown_solutions)) -def subset_helper_block_unknowns(unknowns_all, helper_blocks, helper_targets): - """Find the set of unknowns that the `helper_blocks` solve for""" - unknowns_handled_by_helpers = {} - for block in helper_blocks: - unknowns_handled_by_helpers.update({u: unknowns_all[u] for u in block.outputs if u in unknowns_all}) - - n_unknowns = len(unknowns_handled_by_helpers) - n_targets = len(helper_targets) - if n_unknowns != n_targets: - raise ValueError(f"The provided helper_blocks handle {n_unknowns} unknowns != {n_targets} targets." - f" User must specify an equal number of unknowns/targets solved for by helper blocks.") - - return unknowns_handled_by_helpers - - -def find_excludable_helper_blocks(blocks_all, helper_indices, helper_unknowns, helper_targets): - """Of the set of helper_unknowns and helper_targets, find the ones that can be excluded from the main DAG - for the purposes of numerically solving unknowns.""" - excludable_helper_unknowns = {} - excludable_helper_targets = {} - for i in helper_indices: - excludable_helper_unknowns.update({h: helper_unknowns[h] for h in blocks_all[i].outputs if h in helper_unknowns}) - excludable_helper_targets.update({h: helper_targets[h] for h in blocks_all[i].outputs | blocks_all[i].inputs if h in helper_targets}) - return excludable_helper_unknowns, excludable_helper_targets - - def extract_univariate_initial_values_or_bounds(unknowns): val = next(iter(unknowns.values())) if np.isscalar(val): From 63a4d4ddc6198d6cabbb5ecbc5cc732441966cd1 Mon Sep 17 00:00:00 2001 From: bbardoczy Date: Mon, 11 Oct 2021 12:57:09 -0400 Subject: [PATCH 288/288] CombinedBlock subclasses DAG; simplified initialization, but does not exploit it in methods yet --- .../blocks/combined_block.py | 18 +- src/sequence_jacobian/utilities/graph.py | 162 ------------------ 2 files changed, 5 insertions(+), 175 deletions(-) diff --git a/src/sequence_jacobian/blocks/combined_block.py b/src/sequence_jacobian/blocks/combined_block.py index 14e994a..7980215 100644 --- a/src/sequence_jacobian/blocks/combined_block.py +++ b/src/sequence_jacobian/blocks/combined_block.py @@ -1,12 +1,10 @@ """CombinedBlock class and the combine function to generate it""" -from copy import deepcopy - from .block import Block from .auxiliary_blocks.jacobiandict_block import JacobianDictBlock from .support.parent import Parent from ..classes import ImpulseDict, JacobianDict -from ..utilities.graph import block_sort, find_intermediate_inputs +from ..utilities.graph import DAG, find_intermediate_inputs def combine(blocks, name="", model_alias=False): @@ -18,7 +16,7 @@ def create_model(blocks, **kwargs): return combine(blocks, model_alias=True, **kwargs) -class CombinedBlock(Block, Parent): +class CombinedBlock(Block, Parent, DAG): """A combined `Block` object comprised of several `Block` objects, which topologically sorts them and provides a set of partial and general equilibrium methods for evaluating their steady state, computes impulse responses, and calculates Jacobians along the DAG""" @@ -29,9 +27,10 @@ def __init__(self, blocks, name="", model_alias=False, sorted_indices=None, inte super().__init__() blocks_unsorted = [b if isinstance(b, Block) else JacobianDictBlock(b) for b in blocks] - sorted_indices = block_sort(blocks) if sorted_indices is None else sorted_indices + DAG.__init__(self, blocks_unsorted) + + # TODO: deprecate this, use DAG methods instead self._required = find_intermediate_inputs(blocks) if intermediate_inputs is None else intermediate_inputs - self.blocks = [blocks_unsorted[i] for i in sorted_indices] if not name: self.name = f"{self.blocks[0].name}_to_{self.blocks[-1].name}_combined" @@ -41,13 +40,6 @@ def __init__(self, blocks, name="", model_alias=False, sorted_indices=None, inte # now that it has a name, do Parent initialization Parent.__init__(self, blocks) - # Find all outputs (including those used as intermediary inputs) - self.outputs = set().union(*[block.outputs for block in self.blocks]) - - # Find all inputs that are *not* intermediary outputs - all_inputs = set().union(*[block.inputs for block in self.blocks]) - self.inputs = all_inputs.difference(self.outputs) - # If the create_model() is used instead of combine(), we will have __repr__ show this object as a 'Model' self._model_alias = model_alias diff --git a/src/sequence_jacobian/utilities/graph.py b/src/sequence_jacobian/utilities/graph.py index 709a09e..c37589f 100644 --- a/src/sequence_jacobian/utilities/graph.py +++ b/src/sequence_jacobian/utilities/graph.py @@ -172,168 +172,6 @@ def find_intermediate_inputs(blocks): return required -def block_sort_w_helpers(blocks, helper_blocks=None, calibration=None, return_io=False): - """Given list of blocks (either blocks themselves or dicts of Jacobians), find a topological sort. - - Relies on blocks having 'inputs' and 'outputs' attributes (unless they are dicts of Jacobians, in which case it's - inferred) that indicate their aggregate inputs and outputs - - Importantly, because including helper blocks in a blocks without additional measures - can introduce cycles within the DAG, allow the user to provide the calibration that will be used in the - steady_state computation to resolve these cycles. - e.g. Consider Krusell Smith: - Suppose one specifies a helper block based on a calibrated value for "r", which outputs "K" (among other vars). - Normally block_sort would include the "firm" block as a dependency of the helper block - because the "firm" block outputs "r", which the helper block takes as an input. - However, it would also include the helper block as a dependency of the "firm" block because the "firm" block takes - "K" as an input. - This would result in a cycle. However, if a "calibration" is provided in which "r" is included, then - "firm" could be removed as a dependency of helper block and the cycle would be resolved. - - blocks: `list` - A list of the blocks (SimpleBlock, HetBlock, etc.) to sort - helper_blocks: `list` - A list of helper blocks - calibration: `dict` or `None` - An optional dict of variable/parameter names and their pre-specified values to help resolve any cycles - introduced by using helper blocks. Read above docstring for more detail - return_io: `bool` - A boolean indicating whether to return the full set of input and output arguments from `blocks` - """ - if return_io: - # step 1: map outputs to blocks for topological sort - outmap, outargs = construct_output_map_w_helpers(blocks, return_output_args=True, - helper_blocks=helper_blocks, calibration=calibration) - - # step 2: dependency graph for topological sort and input list - dep, inargs = construct_dependency_graph_w_helpers(blocks, outmap, return_input_args=True, outargs=outargs, - helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(map_to_list(complete_reverse_graph(dep)), map_to_list(dep)), inargs, outargs - else: - # step 1: map outputs to blocks for topological sort - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks) - - # step 2: dependency graph for topological sort and input list - dep = construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=helper_blocks, calibration=calibration) - - return topological_sort(map_to_list(complete_reverse_graph(dep)), map_to_list(dep)) - - -def map_to_list(m): - return [m[i] for i in range(len(m))] - - -def construct_output_map_w_helpers(blocks, helper_blocks=None, calibration=None, return_output_args=False): - """Mirroring construct_output_map functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - - helper_inputs = set().union(*[block.inputs for block in helper_blocks]) - - outmap = dict() - outargs = set() - for num, block in enumerate(blocks): - # Find the relevant set of outputs corresponding to a block - if hasattr(block, "outputs"): - outputs = block.outputs - elif isinstance(block, dict): - outputs = block.keys() - else: - raise ValueError(f'{block} is not recognized as block or does not provide outputs') - - for o in outputs: - # Because some of the outputs of a helper block are, by construction, outputs that also appear in the - # standard blocks that comprise a DAG, ignore the fact that an output is repeated when considering - # throwing this ValueError - if o in outmap and block not in helper_blocks: - raise ValueError(f'{o} is output twice') - - # Priority sorting for standard blocks: - # Ensure that the block "outmap" maps "o" to is the actual block and not a helper block if both share - # a given output, such that the dependency graph is constructed on the standard blocks, where possible - if o not in outmap: - outmap[o] = num - if return_output_args and not (o in helper_inputs and o in calibration): - outargs.add(o) - else: - continue - if return_output_args: - return outmap, outargs - else: - return outmap - - -def construct_dependency_graph_w_helpers(blocks, outmap, helper_blocks=None, - calibration=None, return_input_args=False, outargs=None): - """Mirroring construct_dependency_graph functionality in utilities.graph module but augmented to support - helper blocks""" - if calibration is None: - calibration = {} - if helper_blocks is None: - helper_blocks = [] - if outargs is None: - outargs = {} - - dep = {num: set() for num in range(len(blocks))} - inargs = set() - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - # Each potential input to a given block will either be 1) output by another block, - # 2) an unknown or exogenous variable, or 3) a pre-specified variable/parameter passed into - # the steady-state computation via the `calibration' dict. - # If the block is a helper block, then we want to check the calibration to see if the potential - # input is a pre-specified variable/parameter, and if it is then we will not add the block that - # produces that input as an output as a dependency. - # e.g. Krusell Smith's firm_steady_state_solution helper block and firm block would create a cyclic - # dependency, if it were not for this resolution. - if i in outmap and not (i in calibration and block in helper_blocks): - dep[num].add(outmap[i]) - if return_input_args and not (i in outargs): - inargs.add(i) - if return_input_args: - return dep, inargs - else: - return dep - - -def find_intermediate_inputs_w_helpers(blocks, helper_blocks=None, **kwargs): - """Mirroring find_outputs_that_are_intermediate_inputs functionality in utilities.graph module - but augmented to support helper blocks""" - required = set() - outmap = construct_output_map_w_helpers(blocks, helper_blocks=helper_blocks, **kwargs) - for num, block in enumerate(blocks): - if hasattr(block, 'inputs'): - inputs = block.inputs - else: - inputs = set(i for o in block for i in block[o]) - for i in inputs: - if i in outmap: - required.add(i) - return required - - -def complete_reverse_graph(gph): - """Given directed graph represented as a dict from nodes to iterables of nodes, return representation of graph that - is complete (i.e. has each vertex pointing to some iterable, even if empty), and a complete version of reversed too. - Have returns be sets, for easy removal""" - - revgph = {n: set() for n in gph} - for n, e in gph.items(): - for n2 in e: - n2_edges = revgph.setdefault(n2, set()) - n2_edges.add(n) - - return revgph - - def find_cycle(dep, onlyset=None): """Return list giving cycle if there is one, otherwise None"""