Coverage for HARK / SSJutils.py: 91%

377 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-10 06:19 +0000

1""" 

2Functions for building heterogeneous agent sequence space Jacobian matrices from 

3HARK AgentType instances. The top-level functions are accessible as methods on 

4AgentType itself. 

5""" 

6 

7from time import time 

8from copy import deepcopy 

9import numpy as np 

10from numba import njit 

11 

12 

13def _prepare_ssj_computation(agent, outcomes, grids, norm, solved, verbose): 

14 """ 

15 Shared setup for make_basic_SSJ_matrices and calc_shock_response_manually. 

16 Validates the agent, normalizes outcomes, optionally solves the long run model, 

17 builds transition matrices, and finds the steady state distribution. 

18 

19 Parameters 

20 ---------- 

21 agent : AgentType 

22 Agent for which setup should be performed. 

23 outcomes : str or [str] 

24 Names of outcome variables of interest (will be normalized to a list). 

25 grids : dict 

26 Dictionary of dictionaries with discretizing grid information. 

27 norm : str or None 

28 Name of the model variable to normalize by for Harmenberg aggregation. 

29 solved : bool 

30 Whether the agent's model has already been solved. 

31 verbose : bool 

32 Whether to display timing/progress to screen. 

33 

34 Returns 

35 ------- 

36 setup : dict 

37 Dictionary containing: 

38 - outcomes : [str] normalized list of outcome variable names 

39 - no_list : bool True if outcomes was passed as a single string 

40 - simulator_backup : object agent._simulator backup, or None if not present 

41 - LR_soln : object deepcopy of the long run solution 

42 - X : object reference to agent._simulator 

43 - LR_trans : np.array long run transition matrix 

44 - LR_period : object the long run period object from the simulator 

45 - LR_outcomes : [np.array] outcome matrices in the long run model 

46 - outcome_grids : [np.array] outcome grids in the long run model 

47 - SS_dstn : np.array steady state distribution 

48 """ 

49 if (agent.cycles > 0) or (agent.T_cycle != 1): 

50 raise ValueError( 

51 "This function is only compatible with one period infinite horizon models!" 

52 ) 

53 if not isinstance(outcomes, list): 

54 outcomes = [outcomes] 

55 no_list = True 

56 else: 

57 no_list = False 

58 

59 # Store the simulator if it exists 

60 if hasattr(agent, "_simulator"): 

61 simulator_backup = agent._simulator 

62 else: 

63 simulator_backup = None 

64 

65 # Solve the long run model if it wasn't already 

66 if not solved: 

67 t0 = time() 

68 agent.solve() 

69 t1 = time() 

70 if verbose: 

71 print( 

72 "Solving the long run model took {:.3f}".format(t1 - t0) + " seconds." 

73 ) 

74 LR_soln = deepcopy(agent.solution[0]) 

75 

76 # Construct the transition matrix for the long run model 

77 t0 = time() 

78 agent.initialize_sym() 

79 X = agent._simulator # for easier referencing 

80 X.make_transition_matrices(grids, norm) 

81 LR_trans = X.trans_arrays[0].copy() # the transition matrix in LR model 

82 LR_period = X.periods[0] 

83 LR_outcomes = [] 

84 outcome_grids = [] 

85 for var in outcomes: 

86 if var not in X.periods[0].matrices: 

87 raise ValueError( 

88 "Outcome " + var + " was requested but has no transition matrix." 

89 ) 

90 if var not in X.periods[0].grids: 

91 raise ValueError( 

92 "Outcome " + var + " was requested but no grid was provided!" 

93 ) 

94 LR_outcomes.append(X.periods[0].matrices[var]) 

95 outcome_grids.append(X.periods[0].grids[var]) 

96 t1 = time() 

97 if verbose: 

98 print( 

99 "Making the transition matrix for the long run model took {:.3f}".format( 

100 t1 - t0 

101 ) 

102 + " seconds." 

103 ) 

104 

105 # Find the steady state for the long run model 

106 t0 = time() 

107 X.find_steady_state() 

108 SS_dstn = X.steady_state_dstn.copy() 

109 t1 = time() 

110 if verbose: 

111 print( 

112 "Finding the long run steady state took {:.3f}".format(t1 - t0) 

113 + " seconds." 

114 ) 

115 

116 return { 

117 "outcomes": outcomes, 

118 "no_list": no_list, 

119 "simulator_backup": simulator_backup, 

120 "LR_soln": LR_soln, 

121 "X": X, 

122 "LR_trans": LR_trans, 

123 "LR_period": LR_period, 

124 "LR_outcomes": LR_outcomes, 

125 "outcome_grids": outcome_grids, 

126 "SS_dstn": SS_dstn, 

127 } 

128 

129 

130def _perturb_shock(agent, shock): 

131 """ 

132 Retrieve and validate the named shock attribute on an agent, returning the 

133 scalar base value and a flag indicating whether it is stored as a singleton list. 

134 

135 Parameters 

136 ---------- 

137 agent : AgentType 

138 Agent whose shock attribute will be read. 

139 shock : str 

140 Name of the shock attribute to perturb. 

141 

142 Returns 

143 ------- 

144 base_shock_value : float or np.floating 

145 The scalar value of the shock attribute (unwrapped from a list if needed). 

146 shock_is_list : bool 

147 True if the shock attribute is stored as a list, False otherwise. 

148 """ 

149 if not hasattr(agent, shock): 

150 raise ValueError( 

151 "The agent doesn't have anything called " + shock + " to perturb!" 

152 ) 

153 base = getattr(agent, shock) 

154 if isinstance(base, list): 

155 base_shock_value = base[0] 

156 shock_is_list = True 

157 else: 

158 base_shock_value = base 

159 shock_is_list = False 

160 if isinstance(base_shock_value, bool) or not isinstance( 

161 base_shock_value, (float, np.floating) 

162 ): 

163 raise TypeError( 

164 "The shock attribute '" 

165 + shock 

166 + "' must be a scalar float (Python float or np.floating), but got " 

167 + str(type(base_shock_value)) 

168 + "." 

169 ) 

170 return base_shock_value, shock_is_list 

171 

172 

173def _restore_agent(agent, LR_soln, simulator_backup): 

174 """ 

175 Reset the agent to its original long run state after SSJ or impulse response 

176 computation, restoring the simulator backup if one was saved. 

177 

178 Parameters 

179 ---------- 

180 agent : AgentType 

181 Agent to restore. 

182 LR_soln : object 

183 Long run solution to reinstall as the agent's solution. 

184 simulator_backup : object or None 

185 The original simulator to restore, or None if no backup was stored. 

186 """ 

187 agent.solution = [LR_soln] 

188 agent.cycles = 0 

189 agent._simulator.reset() 

190 if simulator_backup is not None: 

191 agent._simulator = simulator_backup 

192 else: 

193 del agent._simulator 

194 

195 

196def make_basic_SSJ_matrices( 

197 agent, 

198 shock, 

199 outcomes, 

200 grids, 

201 eps=1e-4, 

202 T_max=300, 

203 norm=None, 

204 solved=False, 

205 construct=True, 

206 offset=False, 

207 verbose=False, 

208): 

209 """ 

210 Constructs one or more sequence space Jacobian (SSJ) matrices for specified 

211 outcomes over one shock variable. It is "basic" in the sense that it only 

212 works for "one period infinite horizon" models, as in the original SSJ paper. 

213 

214 Parameters 

215 ---------- 

216 agent : AgentType 

217 Agent for which the SSJ(s) should be constructed. Must have T_cycle=1 

218 and cycles=0, or the function will throw an error. Must have a model 

219 file defined or this won't work at all. 

220 shock : str 

221 Name of the variable that Jacobians will be computed with respect to. 

222 It does not need to be a "shock" in a modeling sense, but it must be a 

223 single-valued parameter (possibly a singleton list) that can be changed. 

224 outcomes : str or [str] 

225 Names of outcome variables of interest; an SSJ matrix will be constructed 

226 for each variable named here. If a single string is passed, the output 

227 will be a single np.array. If a list of strings are passed, the output 

228 will be a list of SSJ matrices in the order specified here. 

229 grids : dict 

230 Dictionary of dictionaries with discretizing grid information. The grids 

231 should include all arrival variables other than those that are normalized 

232 out. They should also include all variables named in outcomes, except 

233 outcomes that are continuation variables that remap to arrival variables. 

234 Grid specification must include number of nodes N, should also include 

235 min and max if the variable is continuous. 

236 eps : float 

237 Amount by which to perturb the shock variable. The default is 1e-4. 

238 T_max : int 

239 Size of the SSJ matrices: the maximum number of periods to consider. 

240 The default is 300. 

241 norm : str or None 

242 Name of the model variable to normalize by for Harmenberg aggregation, 

243 if any. For many HARK models, this should be 'PermShk', which enables 

244 the grid over permanent income to be omitted as an explicit state. 

245 solved : bool 

246 Whether the agent's model has already been solved. If False (default), 

247 it will be solved as the very first step. Solving the agent's long run 

248 model before constructing SSJ matrices has the advantage of not needing 

249 to re-solve the long run model for each shock variable. 

250 construct : bool 

251 Whether the construct (update) method should be run after the shock is 

252 updated. The default is True, which is the "safe" option. If the shock 

253 variable is a parameter that enters the model only *directly*, rather 

254 than being used to build a more complex model input, then this can be 

255 set to False to save a (very) small amount of time during computation. 

256 If it is set to False improperly, the SSJs will be very wrong, potentially 

257 just zero everywhere. 

258 offset : bool 

259 Whether the shock variable is "offset in time" for the solver, with a 

260 default of False. This should be set to True if the named shock variable 

261 (or the constructed model input that it affects) is indexed by t+1 from 

262 the perspective of the solver. For example, the period t solver for the 

263 ConsIndShock model takes in risk free interest factor Rfree as an argument, 

264 but it represents the value of R that will occur at the start of t+1. 

265 verbose : bool 

266 Whether to display timing/progress to screen. The default is False. 

267 

268 Returns 

269 ------- 

270 SSJ : np.array or [np.array] 

271 One or more sequence space Jacobian arrays over the outcome variables 

272 with respect to the named shock variable. 

273 """ 

274 setup = _prepare_ssj_computation(agent, outcomes, grids, norm, solved, verbose) 

275 outcomes = setup["outcomes"] 

276 no_list = setup["no_list"] 

277 simulator_backup = setup["simulator_backup"] 

278 LR_soln = setup["LR_soln"] 

279 LR_trans = setup["LR_trans"] 

280 LR_period = setup["LR_period"] 

281 LR_outcomes = setup["LR_outcomes"] 

282 outcome_grids = setup["outcome_grids"] 

283 SS_dstn = setup["SS_dstn"] 

284 

285 try: 

286 base_shock_value, shock_is_list = _perturb_shock(agent, shock) 

287 Tm1_soln, period_Tm1, period_T = _solve_perturbed_Tm1( 

288 agent, 

289 shock, 

290 base_shock_value, 

291 shock_is_list, 

292 eps, 

293 construct, 

294 LR_soln, 

295 verbose, 

296 ) 

297 _solve_finite_horizon( 

298 agent, 

299 shock, 

300 base_shock_value, 

301 shock_is_list, 

302 T_max, 

303 construct, 

304 Tm1_soln, 

305 verbose, 

306 ) 

307 TmX_trans, TmX_outcomes = _build_finite_horizon_matrices( 

308 agent, 

309 period_Tm1, 

310 period_T, 

311 LR_period, 

312 grids, 

313 norm, 

314 offset, 

315 T_max, 

316 outcomes, 

317 verbose, 

318 ) 

319 

320 J = len(outcomes) 

321 K = SS_dstn.size 

322 D_dstn_array, dY_news_array = _compute_finite_horizon_derivatives( 

323 T_max, 

324 J, 

325 outcomes, 

326 TmX_trans, 

327 TmX_outcomes, 

328 LR_trans, 

329 LR_outcomes, 

330 outcome_grids, 

331 SS_dstn, 

332 verbose, 

333 ) 

334 

335 FN = _build_fake_news( 

336 T_max, 

337 J, 

338 K, 

339 dY_news_array, 

340 D_dstn_array, 

341 LR_trans, 

342 LR_outcomes, 

343 outcome_grids, 

344 verbose, 

345 ) 

346 

347 # Construct the SSJ matrices, one for each outcome variable 

348 t0 = time() 

349 SSJ_array = FN.copy() 

350 for t in range(1, T_max): 

351 SSJ_array[:, 1:, t] += SSJ_array[:, :-1, t - 1] 

352 SSJ_array /= eps 

353 SSJ = [SSJ_array[j, :, :] for j in range(J)] # unpack into a list of arrays 

354 _log_timing(verbose, "Constructing the sequence space Jacobians", t0, time()) 

355 

356 if no_list: 

357 return SSJ[0] 

358 else: 

359 return SSJ 

360 

361 finally: 

362 _restore_agent(agent, LR_soln, simulator_backup) 

363 

364 

365def _log_timing(verbose, label, t0, t1): 

366 if verbose: 

367 print(label + " took {:.3f}".format(t1 - t0) + " seconds.") 

368 

369 

370def _shock_value(base_value, shock_is_list, eps=0.0): 

371 return [base_value + eps] if shock_is_list else base_value + eps 

372 

373 

374def _solve_perturbed_Tm1( 

375 agent, shock, base_shock_value, shock_is_list, eps, construct, LR_soln, verbose 

376): 

377 t0 = time() 

378 agent.cycles = 1 

379 agent.assign_parameters( 

380 **{shock: _shock_value(base_shock_value, shock_is_list, eps)} 

381 ) 

382 if construct: 

383 agent.update() 

384 agent.solve(from_solution=LR_soln) 

385 agent.initialize_sym() 

386 Tm1_soln = deepcopy(agent.solution[0]) 

387 period_Tm1 = agent._simulator.periods[0] 

388 period_T = agent._simulator.periods[-1] 

389 _log_timing(verbose, "Solving period T-1 with a perturbed variable", t0, time()) 

390 return Tm1_soln, period_Tm1, period_T 

391 

392 

393def _solve_finite_horizon( 

394 agent, shock, base_shock_value, shock_is_list, T_max, construct, Tm1_soln, verbose 

395): 

396 t0 = time() 

397 agent.cycles = T_max - 1 

398 agent.assign_parameters(**{shock: _shock_value(base_shock_value, shock_is_list)}) 

399 if construct: 

400 agent.update() 

401 agent.solve(from_solution=Tm1_soln) 

402 _log_timing( 

403 verbose, 

404 "Solving the finite horizon model for " + str(T_max - 1) + " more periods", 

405 t0, 

406 time(), 

407 ) 

408 

409 

410def _build_finite_horizon_matrices( 

411 agent, 

412 period_Tm1, 

413 period_T, 

414 LR_period, 

415 grids, 

416 norm, 

417 offset, 

418 T_max, 

419 outcomes, 

420 verbose, 

421): 

422 t0 = time() 

423 agent.initialize_sym() 

424 X = agent._simulator 

425 X.periods[-1] = period_Tm1 

426 if offset: 

427 for name in X.periods[-1].content.keys(): 

428 if name not in X.solution: 

429 X.periods[-1].content[name] = LR_period.content[name] 

430 X.periods[-1].distribute_content() 

431 X.periods = X.periods[1:] + [period_T] 

432 X.make_transition_matrices(grids, norm, fake_news_timing=True) 

433 TmX_trans = deepcopy(X.trans_arrays) 

434 TmX_outcomes = [ 

435 [X.periods[t].matrices[var] for var in outcomes] for t in range(T_max) 

436 ] 

437 _log_timing( 

438 verbose, 

439 "Constructing transition arrays for the finite horizon model", 

440 t0, 

441 time(), 

442 ) 

443 return TmX_trans, TmX_outcomes 

444 

445 

446def _compute_finite_horizon_derivatives( 

447 T_max, 

448 J, 

449 outcomes, 

450 TmX_trans, 

451 TmX_outcomes, 

452 LR_trans, 

453 LR_outcomes, 

454 outcome_grids, 

455 SS_dstn, 

456 verbose, 

457): 

458 t0 = time() 

459 D_dstn_array = calc_derivs_of_state_dstns( 

460 T_max, J, np.array(TmX_trans), LR_trans, SS_dstn 

461 ) 

462 dY_news_array = np.empty((T_max, J)) 

463 for j in range(J): 

464 temp_outcomes = np.array([TmX_outcomes[t][j] for t in range(T_max)]) 

465 dY_news_array[:, j] = calc_derivs_of_policy_funcs( 

466 T_max, temp_outcomes, LR_outcomes[j], outcome_grids[j], SS_dstn 

467 ) 

468 _log_timing(verbose, "Calculating derivatives by first differences", t0, time()) 

469 return D_dstn_array, dY_news_array 

470 

471 

472def _build_fake_news( 

473 T_max, 

474 J, 

475 K, 

476 dY_news_array, 

477 D_dstn_array, 

478 LR_trans, 

479 LR_outcomes, 

480 outcome_grids, 

481 verbose, 

482): 

483 t0 = time() 

484 expectation_vectors = np.empty((J, K)) 

485 for j in range(J): 

486 expectation_vectors[j, :] = np.dot(LR_outcomes[j], outcome_grids[j]) 

487 FN = make_fake_news_matrices( 

488 T_max, 

489 J, 

490 dY_news_array, 

491 D_dstn_array, 

492 LR_trans.T, 

493 expectation_vectors.copy(), 

494 ) 

495 _log_timing(verbose, "Constructing the fake news matrices", t0, time()) 

496 return FN 

497 

498 

499def make_flat_LC_SSJ_matrices( 

500 agent, 

501 shock, 

502 outcomes, 

503 grids, 

504 eps=1e-4, 

505 T_max=100, 

506 norm=None, 

507 trend=None, 

508 pop_gro=1.0, 

509 prod_gro=1.0, 

510 solved=False, 

511 age_agg=True, 

512 construct=True, 

513 offset=False, 

514 verbose=False, 

515): 

516 """ 

517 Constructs one or more sequence space Jacobian (SSJ) matrices for specified 

518 outcomes over one shock variable. This version of the function is for life- 

519 cycle models with "flat" demographic dynamics: the long run distribution of 

520 ages is stable. This requires that survival probability is not endogenous to 

521 agent actions and thus cannot be affected by shocks. 

522 

523 "Flat" demographic dynamics permit two very specific growth trends: constant 

524 population growth and constant aggregate productivity growth. 

525 

526 This algorithm (and some of the code) are directly taken from Mateo Velasquez 

527 and Bence Bardoczy's paper on life-cycle Jacobians, and its accompanying repo. 

528 

529 Parameters 

530 ---------- 

531 agent : AgentType 

532 Agent for which the SSJ(s) should be constructed. Must have cycles = 1 

533 or the function will throw an error. Must have a model file defined or 

534 this won't work at all. 

535 shock : str 

536 Name of the variable that Jacobians will be computed with respect to. 

537 It does not need to be a "shock" in a modeling sense, but it must be a 

538 single-valued parameter (possibly a singleton list) that can be changed. 

539 outcomes : str or [str] 

540 Names of outcome variables of interest; an SSJ matrix will be constructed 

541 for each variable named here. If a single string is passed, the output 

542 will be a single np.array. If a list of strings are passed, the output 

543 will be a list of SSJ matrices in the order specified here. 

544 grids : dict 

545 Dictionary of dictionaries with discretizing grid information. The grids 

546 should include all arrival variables other than those that are normalized 

547 out. They should also include all variables named in outcomes, except 

548 outcomes that are continuation variables that remap to arrival variables. 

549 Grid specification must include number of nodes N, should also include 

550 min and max if the variable is continuous. 

551 eps : float 

552 Amount by which to perturb the shock variable. The default is 1e-4. 

553 T_max : int 

554 Size of the SSJ matrices: the maximum number of periods to consider. 

555 The default is 100. 

556 norm : str or None 

557 Name of the model variable to normalize by for Harmenberg aggregation, 

558 if any. For many HARK models, this should be 'PermShk', which enables 

559 the grid over permanent income to be omitted as an explicit state. 

560 trend : str or None 

561 Name of the model variable that represents the "normalization trend factor" 

562 for the outcomes. For example, most consumption-saving models in HARK are 

563 normalized by permanent income, which grows by factor `PermGroFac` each 

564 period of the life-cycle; `PermGroFac` should be named as the `trend` for 

565 any model outputs that are normalized by permanent income (i.e. they have 

566 `Nrm` in their name). In contrast, if you wanted the fraction of agents 

567 that have `Lbr > 0.0` for `LaborIntMargConsumerType`, a binary indicator 

568 for this outcome should *not* have `PermGroFac` named as the `trend`-- 

569 you don't want to upweight people who have accumulated more income growth 

570 more when calculating the employment rate! 

571 pop_gro : float 

572 Constant population growth factor, defaulting to 1. Each successive 

573 birth cohort is this factor bigger than the prior birth cohort. With flat 

574 demographic dynamics, this results in the entire population growing by 

575 this factor each period as well. NOT YET IMPLEMENTED 

576 prod_gro : float 

577 Constant aggregate productivity growth factor, defaulting to 1. Each 

578 successive birth cohort has permanent labor productivity that is this 

579 factor bigger than the prior cohort. With flat demographic dynamics, 

580 this results in aggregate productivity growing by this factor as well. 

581 NOT YET IMPLEMENTED 

582 solved : bool 

583 Whether the agent's model has already been solved. If False (default), 

584 it will be solved as the very first step. Solving the agent's long run 

585 model before constructing SSJ matrices has the advantage of not needing 

586 to re-solve the long run model for each shock variable. 

587 age_agg : bool 

588 Whether the returned SSJs should combine effects across ages (default True) 

589 or leave effects disaggregated by age (False). When False, each SSJ will 

590 be shape (T_age, T_max, T_max), and the overall SSJ matrix can be found by 

591 doing np.sum(SSJ, axis=0). 

592 construct : bool 

593 Whether the construct (update) method should be run after the shock is 

594 updated. The default is True, which is the "safe" option. If the shock 

595 variable is a parameter that enters the model only *directly*, rather 

596 than being used to build a more complex model input, then this can be 

597 set to False to save a (very) small amount of time during computation. 

598 If it is set to False improperly, the SSJs will be very wrong, potentially 

599 just zero everywhere. 

600 offset : bool 

601 Whether the shock variable is "offset in time" for the solver, with a 

602 default of False. This should be set to True if the named shock variable 

603 (or the constructed model input that it affects) is indexed by t+1 from 

604 the perspective of the solver. For example, the period t solver for the 

605 ConsIndShock model takes in risk free interest factor Rfree as an argument, 

606 but it represents the value of R that will occur at the start of t+1. 

607 verbose : bool 

608 Whether to display timing/progress to screen. The default is False. 

609 

610 Returns 

611 ------- 

612 SSJ : np.array or [np.array] 

613 One or more sequence space Jacobian arrays over the outcome variables 

614 with respect to the named shock variable. Each is shape (T_max, T_max). 

615 """ 

616 if agent.cycles != 1: 

617 raise ValueError("This function is only compatible with life-cycle models!") 

618 if not isinstance(outcomes, list): 

619 outcomes = [outcomes] 

620 no_list = True 

621 else: 

622 no_list = False 

623 J = len(outcomes) 

624 

625 # Check for attempts to use future functionality 

626 if pop_gro != 1.0: 

627 raise ValueError( 

628 "Population growth is not yet implemented for make_flat_LC_SSJ_matrices!" 

629 ) 

630 if prod_gro != 1.0: 

631 raise ValueError( 

632 "Productivity growth is not yet implemented for make_flat_LC_SSJ_matrices!" 

633 ) 

634 

635 # Store the simulator if it exists 

636 simulator_backup = agent._simulator if hasattr(agent, "_simulator") else None 

637 

638 # Make sure the shock variable is age-varying 

639 if shock in agent.time_inv: 

640 temp = getattr(agent, shock) 

641 original_shock_value = temp 

642 setattr(agent, shock, agent.T_cycle * [temp]) 

643 agent.del_from_time_inv(shock) 

644 agent.add_to_time_vary(shock) 

645 shock_was_time_inv = True 

646 else: 

647 shock_was_time_inv = False 

648 

649 # Solve the long run model if it wasn't already 

650 if not solved: 

651 t0 = time() 

652 agent.solve() 

653 t1 = time() 

654 if verbose: 

655 print( 

656 "Solving the long run model took {:.3f}".format(t1 - t0) + " seconds." 

657 ) 

658 LR_soln = deepcopy(agent.solution) 

659 

660 try: 

661 t0 = time() 

662 agent.initialize_sym() 

663 X = agent._simulator # for easier referencing 

664 

665 # Construct the transition matrices for the long run model 

666 X.make_transition_matrices(grids, norm) 

667 LR_trans = deepcopy(X.trans_arrays) # the transition matrices in LR model 

668 T_age = len(LR_trans) 

669 if T_max < T_age: 

670 raise ValueError( 

671 "T_max must be greater than or equal to T_age in order to pad " 

672 "fake_news_array without truncation." 

673 ) 

674 LR_outcomes = [] 

675 outcome_grids = [] 

676 for var in outcomes: 

677 try: 

678 LR_outcomes.append([X.periods[t].matrices[var] for t in range(T_age)]) 

679 outcome_grids.append([X.periods[t].grids[var] for t in range(T_age)]) 

680 except KeyError as exc: 

681 raise KeyError( 

682 "Outcome " + var + " was requested, but no grid was provided!" 

683 ) from exc 

684 

685 # Extract the normalizing trend 

686 if trend is not None: 

687 trend_adj_fac = np.array( 

688 [X.periods[t].content[trend] for t in range(T_age)] 

689 ) 

690 trend_adj_fac[0] = 1.0 

691 trend_adj_cum = np.cumprod(trend_adj_fac) 

692 else: 

693 trend_adj_cum = np.ones(T_age) 

694 

695 t1 = time() 

696 if verbose: 

697 print( 

698 "Making the transition matrix for the long run model took {:.3f}".format( 

699 t1 - t0 

700 ) 

701 + " seconds." 

702 ) 

703 

704 # Find the steady state for the long run model 

705 t0 = time() 

706 X.simulate_cohort_by_grids(outcomes=["dead"] + outcomes, calc_dstn=True) 

707 SS_dstn = deepcopy(X.state_dstn_by_age) 

708 SS_outcomes = {} 

709 for j in range(J): 

710 name = outcomes[j] 

711 SS_outcomes[name] = [ 

712 np.dot(LR_outcomes[j][t], outcome_grids[j][t]) for t in range(T_age) 

713 ] 

714 

715 # Re-apply mortality to downweight older ages 

716 survival_by_age = 1.0 - X.history_avg["dead"] 

717 survival_by_age[-1] = 0.0 # Force automatic death 

718 cum_liv_prb = 1.0 

719 pop_sum = 0.0 

720 for a in range(T_age): 

721 SS_dstn[a] *= cum_liv_prb 

722 pop_sum += cum_liv_prb 

723 cum_liv_prb *= survival_by_age[a] 

724 

725 t1 = time() 

726 if verbose: 

727 print( 

728 "Finding the long run steady state took {:.3f}".format(t1 - t0) 

729 + " seconds." 

730 ) 

731 

732 # Construct the "expectation vectors" for all outcomes at all ages 

733 t0 = time() 

734 E_vecs = {} 

735 for j in range(J): 

736 name = outcomes[j] 

737 E_temp = [[SS_outcomes[name][a].copy()] for a in range(T_age)] 

738 for t in range(1, T_age): 

739 for a in range(T_age - t): 

740 S = survival_by_age[a] 

741 E_temp[a].append(np.dot(S * LR_trans[a], E_temp[a + 1][-1])) 

742 E_vecs[name] = E_temp 

743 t1 = time() 

744 

745 # Rearrange the expectation vectors for better access later 

746 E_curly = [ 

747 np.stack( 

748 [ 

749 np.stack([E_vecs[name][a][t] for name in outcomes]) 

750 for t in range(T_age - a) 

751 ] 

752 ) 

753 for a in range(T_age) 

754 ] 

755 

756 if verbose: 

757 print( 

758 "Constructing expectation vectors took {:.3f}".format(t1 - t0) 

759 + " seconds." 

760 ) 

761 

762 # Each entry of the E_vecs dictionary is a nested list. The outer index of the 

763 # list is a, the age at t=0, and the inner index is time period t. The elements 

764 # in the nested list are expectation vectors: the expected value of the outcome 

765 # in period t conditional on being age a and at state space gridpoint n at t=0. 

766 

767 # Initialize the fake news matrices for each output 

768 fake_news_array = np.zeros((J, T_age, T_age, T_age)) 

769 # Dimensions of fake news arrays: 

770 # dim 0 --> j: index of outcome variable 

771 # dim 1 --> a: age in period t 

772 # dim 2 --> t: periods since news arrived 

773 # dim 3 --> s: periods ahead about which the news arrived 

774 

775 # Loop over ages of the model and have the news shock apply at each one; 

776 # k is the age index at which the shock arrives 

777 t0 = time() 

778 for k in reversed(range(T_age)): 

779 # Adjust the timing for "offset" shocks 

780 l = k - int(offset) 

781 shock_val_orig = getattr(agent, shock)[l] 

782 shock_val_new = shock_val_orig + eps 

783 

784 # Perturb the shock variable at age k, which corresponds to "solver period" l 

785 if l >= 0: 

786 getattr(agent, shock)[l] = shock_val_new 

787 

788 # Solve the model backwards from age l 

789 if construct: 

790 agent.construct() 

791 agent.solve(from_solution=LR_soln[l + 1], from_t=l + 1) 

792 else: 

793 agent.solution = LR_soln 

794 

795 # Build transitions and outcomes up to age k. Don't use "fake news timing" option! 

796 agent.initialize_sym() 

797 X = agent._simulator # for easier typing 

798 if l < 0: 

799 setattr(X.periods[0], shock, shock_val_new) 

800 X.make_transition_matrices(grids, norm, for_t=range(k + 1)) 

801 shocked_trans = deepcopy(X.trans_arrays) 

802 shocked_outcomes = [] 

803 for var in outcomes: 

804 temp_outcomes = [] 

805 for a in range(k + 1): 

806 temp_outcomes.append(X.periods[a].matrices[var]) 

807 shocked_outcomes.append(temp_outcomes) 

808 

809 # Update the t=0 row of the fake news matrices 

810 for j in range(J): 

811 for a in range(k + 1): 

812 temp = np.dot( 

813 SS_dstn[a], shocked_outcomes[j][a] - LR_outcomes[j][a] 

814 ) 

815 fake_news_array[j, a, 0, k - a] += np.dot(temp, outcome_grids[j][a]) 

816 

817 # Update the other t rows of the fake news matrices 

818 for a in range(k + 1): 

819 if a >= T_age - 1: 

820 continue 

821 S = survival_by_age[a] 

822 D_dstn_news = ( 

823 np.dot(S * shocked_trans[a].T, SS_dstn[a]) - SS_dstn[a + 1] 

824 ) 

825 update_FN_mats( 

826 fake_news_array, E_curly[a + 1], D_dstn_news, T_age, a, k 

827 ) 

828 

829 # Reset the shock variable at age l 

830 if l >= 0: 

831 getattr(agent, shock)[l] = shock_val_orig 

832 

833 t1 = time() 

834 if verbose: 

835 print( 

836 "Making fake news arrays for each period of the problem took {:.3f}".format( 

837 t1 - t0 

838 ) 

839 + " seconds." 

840 ) 

841 

842 t0 = time() 

843 # Pad out the fake news array with zeros 

844 FN_pad = np.zeros((J, T_age, T_max, T_max)) 

845 FN_pad[:, :, :T_age, :T_age] = fake_news_array 

846 

847 # Construct age-specific Jacobian matrices 

848 SSJ_by_age = FN_pad.copy() 

849 for t in range(1, T_max): 

850 SSJ_by_age[:, :, 1:, t] += SSJ_by_age[:, :, :-1, t - 1] 

851 

852 # Apply normalization factors 

853 SSJ_by_age *= np.reshape(trend_adj_cum, (1, T_age, 1, 1)) 

854 SSJ_by_age /= pop_sum * eps 

855 

856 t1 = time() 

857 if verbose: 

858 print( 

859 "Constructing the sequence space Jacobians took {:.3f}".format(t1 - t0) 

860 + " seconds." 

861 ) 

862 

863 # Structure and return outputs, aggregating by age if requested 

864 SSJ = [SSJ_by_age[j, :, :, :] for j in range(J)] 

865 if age_agg: 

866 for j in range(J): 

867 SSJ[j] = np.sum(SSJ[j], axis=0) 

868 if no_list: 

869 return SSJ[0] 

870 else: 

871 return SSJ 

872 

873 finally: 

874 # Make sure the agent wasn't unexpectedly mutated in this method 

875 _restore_agent(agent, LR_soln, simulator_backup) 

876 if shock_was_time_inv: 

877 setattr(agent, shock, original_shock_value) 

878 agent.del_from_time_vary(shock) 

879 agent.add_to_time_inv(shock) 

880 

881 

882def calc_shock_response_manually( 

883 agent, 

884 shock, 

885 outcomes, 

886 grids, 

887 s=0, 

888 eps=1e-4, 

889 T_max=300, 

890 norm=None, 

891 solved=False, 

892 construct=[], 

893 offset=False, 

894 verbose=False, 

895): 

896 """ 

897 Compute an AgentType instance's timepath of outcome responses to learning at 

898 t=0 that the named shock variable will be perturbed at t=s. This is equivalent 

899 to calculating only the s-th column of the SSJs *manually*, rather than using 

900 the fake news algorithm. This function can be used to verify and/or debug the 

901 output of the fake news SSJ algorithm. 

902 

903 Important: Mortality (or death and replacement generally) should be turned 

904 off in the model (via parameter values) for this to work properly. Or does it? 

905 

906 Parameters 

907 ---------- 

908 agent : AgentType 

909 Agent for which the response(s) should be calculated. Must have T_cycle=1 

910 and cycles=0, or the function will throw an error. Must have a model 

911 file defined or this won't work at all. 

912 shock : str 

913 Name of the variable that the response will be computed with respect to. 

914 It does not need to be a "shock" in a modeling sense, but it must be a 

915 single-valued parameter (possibly a singleton list) that can be changed. 

916 outcomes : str or [str] 

917 Names of outcome variables of interest; an SSJ matrix will be constructed 

918 for each variable named here. If a single string is passed, the output 

919 will be a single np.array. If a list of strings are passed, the output 

920 will be a list of dYdX vectors in the order specified here. 

921 grids : dict 

922 Dictionary of dictionaries with discretizing grid information. The grids 

923 should include all arrival variables other than those that are normalized 

924 out. They should also include all variables named in outcomes, except 

925 outcomes that are continuation variables that remap to arrival variables. 

926 Grid specification must include number of nodes N, should also include 

927 min and max if the variable is continuous. 

928 s : int 

929 Period in which the shock variable is perturbed, relative to current t=0. 

930 The default is 0. 

931 eps : float 

932 Amount by which to perturb the shock variable. The default is 1e-4. 

933 T_max : int 

934 The length of the simulation for this exercise. The default is 300. 

935 norm : str or None 

936 Name of the model variable to normalize by for Harmenberg aggregation, 

937 if any. For many HARK models, this should be 'PermShk', which enables 

938 the grid over permanent income to be omitted as an explicit state. 

939 solved : bool 

940 Whether the agent's model has already been solved. If False (default), 

941 it will be solved as the very first step. 

942 construct : [str] 

943 List of constructed objects that will be changed by perturbing shock. 

944 These should all share an "offset status" (True or False). Default is []. 

945 offset : bool 

946 Whether the shock variable is "offset in time" for the solver, with a 

947 default of False. This should be set to True if the named shock variable 

948 (or the constructed model input that it affects) is indexed by t+1 from 

949 the perspective of the solver. For example, the period t solver for the 

950 ConsIndShock model takes in risk free interest factor Rfree as an argument, 

951 but it represents the value of R that will occur at the start of t+1. 

952 verbose : bool 

953 Whether to display timing/progress to screen. The default is False. 

954 

955 Returns 

956 ------- 

957 dYdX : np.array or [np.array] 

958 One or more vectors of length T_max. 

959 """ 

960 setup = _prepare_ssj_computation(agent, outcomes, grids, norm, solved, verbose) 

961 outcomes = setup["outcomes"] 

962 no_list = setup["no_list"] 

963 simulator_backup = setup["simulator_backup"] 

964 LR_soln = setup["LR_soln"] 

965 X = setup["X"] 

966 LR_outcomes = setup["LR_outcomes"] 

967 outcome_grids = setup["outcome_grids"] 

968 SS_dstn = setup["SS_dstn"] 

969 

970 SS_outcomes = [np.dot(mat.T, SS_dstn) for mat in LR_outcomes] 

971 SS_avgs = [np.dot(ss, grid) for ss, grid in zip(SS_outcomes, outcome_grids)] 

972 

973 try: 

974 # Make a temporary agent to construct the perturbed constructed objects 

975 t0 = time() 

976 temp_agent = deepcopy(agent) 

977 base_shock_value, shock_is_list = _perturb_shock(agent, shock) 

978 if shock_is_list: 

979 temp_value = [base_shock_value + eps] 

980 else: 

981 temp_value = base_shock_value + eps 

982 temp_dict = {shock: temp_value} 

983 temp_agent.assign_parameters(**temp_dict) 

984 if len(construct) > 0: 

985 temp_agent.update() 

986 for var in construct: 

987 temp_dict[var] = getattr(temp_agent, var) 

988 

989 # Build the finite horizon version of this agent 

990 FH_agent = deepcopy(agent) 

991 FH_agent.del_param("solution") 

992 FH_agent.del_param("_simulator") 

993 FH_agent.del_from_time_vary("solution") 

994 FH_agent.del_from_time_inv(shock) 

995 FH_agent.add_to_time_vary(shock) 

996 FH_agent.del_from_time_inv(*construct) 

997 FH_agent.add_to_time_vary(*construct) 

998 finite_dict = {"T_cycle": T_max, "cycles": 1} 

999 for var in FH_agent.time_vary: 

1000 if var in construct: 

1001 sequence = [deepcopy(getattr(agent, var)[0]) for t in range(T_max)] 

1002 sequence[s] = deepcopy(getattr(temp_agent, var)[0]) 

1003 else: 

1004 sequence = T_max * [deepcopy(getattr(agent, var)[0])] 

1005 finite_dict[var] = sequence 

1006 shock_seq = T_max * [base_shock_value] 

1007 shock_seq[s] = base_shock_value + eps 

1008 finite_dict[shock] = shock_seq 

1009 FH_agent.assign_parameters(**finite_dict) 

1010 del temp_agent 

1011 t1 = time() 

1012 if verbose: 

1013 print( 

1014 "Building the finite horizon agent took {:.3f}".format(t1 - t0) 

1015 + " seconds." 

1016 ) 

1017 

1018 # Solve the finite horizon agent 

1019 t0 = time() 

1020 FH_agent.solve(from_solution=LR_soln) 

1021 t1 = time() 

1022 if verbose: 

1023 print( 

1024 "Solving the " 

1025 + str(T_max) 

1026 + " period problem took {:.3f}".format(t1 - t0) 

1027 + " seconds." 

1028 ) 

1029 

1030 # Build transition matrices for the finite horizon problem 

1031 t0 = time() 

1032 FH_agent.initialize_sym() 

1033 FH_agent._simulator.make_transition_matrices( 

1034 grids, norm=norm, fake_news_timing=True 

1035 ) 

1036 t1 = time() 

1037 if verbose: 

1038 print( 

1039 "Constructing transition matrices took {:.3f}".format(t1 - t0) 

1040 + " seconds." 

1041 ) 

1042 

1043 # Use grid simulation to find the timepath of requested variables, and compute 

1044 # the derivative with respect to baseline outcomes 

1045 t0 = time() 

1046 FH_agent._simulator.simulate_cohort_by_grids(outcomes, from_dstn=SS_dstn) 

1047 dYdX = [] 

1048 for j, var in enumerate(outcomes): 

1049 diff_path = (FH_agent._simulator.history_avg[var] - SS_avgs[j]) / eps 

1050 if offset: 

1051 dYdX.append(diff_path[1:]) 

1052 else: 

1053 dYdX.append(diff_path[:-1]) 

1054 t1 = time() 

1055 if verbose: 

1056 print( 

1057 "Calculating impulse responses by grid simulation took {:.3f}".format( 

1058 t1 - t0 

1059 ) 

1060 + " seconds." 

1061 ) 

1062 

1063 del FH_agent 

1064 if no_list: 

1065 return dYdX[0] 

1066 else: 

1067 return dYdX 

1068 finally: 

1069 _restore_agent(agent, LR_soln, simulator_backup) 

1070 

1071 

1072@njit 

1073def calc_derivs_of_state_dstns(T, J, trans_by_t, trans_LR, SS_dstn): # pragma: no cover 

1074 """ 

1075 Numba-compatible helper function to calculate the derivative of the state 

1076 distribution by period. 

1077 

1078 Parameters 

1079 ---------- 

1080 T : int 

1081 Maximum time horizon for the fake news algorithm. 

1082 J : int 

1083 Number of outcomes of interest. 

1084 trans_by_t : np.array 

1085 Array of shape (T,K,K) representing the transition matrix in each period. 

1086 trans_LR : np.array 

1087 Array of shape (K,K) representing the long run transition matrix. 

1088 SS_dstn : np.array 

1089 Array of size K representing the long run steady state distribution. 

1090 

1091 Returns 

1092 ------- 

1093 D_dstn_news : np.array 

1094 Array of shape (T,K) representing dD_1^s from the SSJ paper, where K 

1095 is the number of arrival state space nodes. 

1096 

1097 """ 

1098 K = SS_dstn.size 

1099 D_dstn_news = np.empty((T, K)) # this is dD_1^s in the SSJ paper (equation 24) 

1100 for t in range(T - 1, -1, -1): 

1101 D_dstn_news[T - t - 1, :] = np.dot((trans_by_t[t, :, :] - trans_LR).T, SS_dstn) 

1102 return D_dstn_news 

1103 

1104 

1105@njit 

1106def calc_derivs_of_policy_funcs(T, Y_by_t, Y_LR, Y_grid, SS_dstn): # pragma: no cover 

1107 """ 

1108 Numba-compatible helper function to calculate the derivative of an outcome 

1109 function in each period. 

1110 

1111 Parameters 

1112 ---------- 

1113 T : int 

1114 Maximum time horizon for the fake news algorithm. 

1115 Y_by_t : np.array 

1116 Array of shape (T,K,N) with the stochastic outcome, mapping from K arrival 

1117 state space nodes to N outcome space nodes, for each of the T periods. 

1118 Y_LR : np.array 

1119 Array of shape (K,N) representing the stochastic outcome in the long run. 

1120 Y_grid : np.array 

1121 Array of size N representing outcome space gridpoints. 

1122 SS_dstn : np.array 

1123 Array of size K representing the long run steady state distribution. 

1124 

1125 Returns 

1126 ------- 

1127 dY_news : np.array 

1128 Array of size T representing the change in average outcome in each period 

1129 when the shock arrives unexpectedly in that period. 

1130 """ 

1131 dY_news = np.empty(T) # this is dY_0^s in the SSJ paper (equation 24) 

1132 for t in range(T - 1, -1, -1): 

1133 temp = (Y_by_t[t, :, :] - Y_LR).T 

1134 dY_news[T - t - 1] = np.dot(np.dot(temp, SS_dstn), Y_grid) 

1135 return dY_news 

1136 

1137 

1138@njit 

1139def make_fake_news_matrices(T, J, dY, D_dstn, trans_LR, E): # pragma: no cover 

1140 """ 

1141 Numba-compatible function to calculate the fake news array from first order 

1142 perturbation information. 

1143 

1144 Parameters 

1145 ---------- 

1146 T : int 

1147 Maximum time horizon for the fake news algorithm. 

1148 J : int 

1149 Number of outcomes of interest. 

1150 dY : int 

1151 Array shape (T,J) representing dY_0 from the SSJ paper. 

1152 D_dstn : np.array 

1153 Array of shape (T,K) representing dD_1^s from the SSJ paper, where K 

1154 is the number of arrival state space nodes. 

1155 trans_LR : np.array 

1156 Array of shape (K,K) representing the transpose of the long run transition matrix. 

1157 E : np.array 

1158 Initial expectation vectors combined into a single array of shape (J,K). 

1159 

1160 Returns 

1161 ------- 

1162 FN : np.array 

1163 Fake news array of shape (J,T,T). 

1164 """ 

1165 FN = np.empty((J, T, T)) 

1166 FN[:, 0, :] = dY.T # Fill in row zero 

1167 for t in range(1, T): # Loop over other rows 

1168 for s in range(T): 

1169 FN[:, t, s] = np.dot(E, D_dstn[s, :]) 

1170 E = np.dot(E, trans_LR) 

1171 return FN 

1172 

1173 

1174@njit 

1175def update_FN_mats(FN_mats, evecs, dD1, A, a, k): # pragma: no cover 

1176 """ 

1177 This is adapted from Mateo's code. 

1178 

1179 FN_mats: (J, A, A, A) 

1180 evecs : (T, J, G) 

1181 dD1 : (G,) 

1182 """ 

1183 J = FN_mats.shape[0] 

1184 G = dD1.shape[0] 

1185 

1186 for j in range(J): 

1187 for t in range(1, A - a): 

1188 # compute dot(evecs[t-1, j, :], dD1) by hand 

1189 v = 0.0 

1190 for g in range(G): 

1191 v += evecs[t - 1, j, g] * dD1[g] 

1192 FN_mats[j, a + t, t, k - a] += v