From 716d59c93d104b137a4c7da8c513c5b3457b7509 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 04:48:40 +0000 Subject: [PATCH] promote: refresh 14 pool lectures whose canonical source moved --- lectures/career.md | 394 +++++++++++++------------- lectures/ifp_advanced.md | 26 +- lectures/ifp_egm.md | 2 +- lectures/ifp_egm_transient_shocks.md | 4 +- lectures/inventory_q.md | 29 +- lectures/jv.md | 401 +++++++++++++++------------ lectures/mccall_fitted_vfi.md | 8 +- lectures/mccall_model.md | 10 +- lectures/mccall_persist_trans.md | 6 +- lectures/mccall_q.md | 72 +++-- lectures/opt_tax_recur.md | 4 +- lectures/os_stochastic.md | 4 +- lectures/os_time_iter.md | 4 +- lectures/rs_inventory_q.md | 28 +- sync/ledger.yml | 274 ++++++++++-------- 15 files changed, 677 insertions(+), 589 deletions(-) diff --git a/lectures/career.md b/lectures/career.md index 19fd51f..f3ff7f7 100644 --- a/lectures/career.md +++ b/lectures/career.md @@ -18,7 +18,7 @@ kernelspec: ``` -# Job Search VI: Modeling Career Choice +# Job Search VII: Modeling Career Choice ```{index} single: Modeling; Career Choice ``` @@ -27,13 +27,16 @@ kernelspec: :depth: 2 ``` +```{include} _static/_shared/_admonition/gpu.md +``` + In addition to what's in Anaconda, this lecture will need the following libraries: ```{code-cell} ipython --- tags: [hide-output] --- -!pip install quantecon +!pip install quantecon jax ``` ## Overview @@ -46,15 +49,16 @@ This exposition draws on the presentation in {cite}`Ljungqvist2012`, section 6.5 We begin with some imports: -```{code-cell} ipython +```{code-cell} ipython3 +from typing import NamedTuple + import matplotlib.pyplot as plt -import numpy as np -import quantecon as qe -from numba import jit, prange -from quantecon.distributions import BetaBinomial -from scipy.special import binom, beta -from mpl_toolkits.mplot3d.axes3d import Axes3D from matplotlib import cm +from mpl_toolkits.mplot3d.axes3d import Axes3D +import jax +import jax.numpy as jnp +import jax.random as jr +from quantecon.distributions import BetaBinomial ``` ### Model Features @@ -132,14 +136,14 @@ Evidently $I$, $II$ and $III$ correspond to "stay put", "new job" and "new life" As in {cite}`Ljungqvist2012`, section 6.5, we will focus on a discrete version of the model, parameterized as follows: * both $\theta$ and $\epsilon$ take values in the set - `np.linspace(0, B, grid_size)` --- an even grid of points between + `jnp.linspace(0, B, grid_size)` --- an even grid of points between $0$ and $B$ inclusive * `grid_size = 50` * `B = 5` * `β = 0.95` The distributions $F$ and $G$ are discrete distributions -generating draws from the grid points `np.linspace(0, B, grid_size)`. +generating draws from the grid points `jnp.linspace(0, B, grid_size)`. A very useful family of discrete distributions is the Beta-binomial family, with probability mass function @@ -163,161 +167,159 @@ Nice properties: Here's a figure showing the effect on the pmf of different shape parameters when $n=50$. -```{code-cell} python3 -def gen_probs(n, a, b): - probs = np.zeros(n+1) - for k in range(n+1): - probs[k] = binom(n, k) * beta(k + a, n - k + b) / beta(a, b) - return probs - +```{code-cell} ipython3 n = 50 a_vals = [0.5, 1, 100] b_vals = [0.5, 1, 100] + fig, ax = plt.subplots(figsize=(10, 6)) for a, b in zip(a_vals, b_vals): ab_label = f'$a = {a:.1f}$, $b = {b:.1f}$' - ax.plot(list(range(0, n+1)), gen_probs(n, a, b), '-o', label=ab_label) + ax.plot(range(n + 1), BetaBinomial(n, a, b).pdf(), '-o', label=ab_label) ax.legend() plt.show() ``` ## Implementation -We will first create a class `CareerWorkerProblem` which will hold the -default parameterizations of the model and an initial guess for the value function. - -```{code-cell} python3 -class CareerWorkerProblem: - - def __init__(self, - B=5.0, # Upper bound - β=0.95, # Discount factor - grid_size=50, # Grid size - F_a=1, - F_b=1, - G_a=1, - G_b=1): - - self.β, self.grid_size, self.B = β, grid_size, B - - self.θ = np.linspace(0, B, grid_size) # Set of θ values - self.ϵ = np.linspace(0, B, grid_size) # Set of ϵ values - - self.F_probs = BetaBinomial(grid_size - 1, F_a, F_b).pdf() - self.G_probs = BetaBinomial(grid_size - 1, G_a, G_b).pdf() - self.F_mean = self.θ @ self.F_probs - self.G_mean = self.ϵ @ self.G_probs - - # Store these parameters for str and repr methods - self._F_a, self._F_b = F_a, F_b - self._G_a, self._G_b = G_a, G_b +We store the model primitives in a `NamedTuple`, built by a factory function. + +```{code-cell} ipython3 +class CareerWorkerProblem(NamedTuple): + β: float # Discount factor + θ: jnp.ndarray # Set of θ values (career) + ϵ: jnp.ndarray # Set of ϵ values (job) + F_probs: jnp.ndarray # Distribution over new career draws + G_probs: jnp.ndarray # Distribution over new job draws + F_mean: float # Mean of F + G_mean: float # Mean of G + + +def create_career_worker_problem(B=5.0, # Upper bound + β=0.95, # Discount factor + grid_size=50, # Grid size + F_a=1, + F_b=1, + G_a=1, + G_b=1): + "Create an instance of the career choice model." + θ = jnp.linspace(0, B, grid_size) + ϵ = jnp.linspace(0, B, grid_size) + + F_probs = jnp.array(BetaBinomial(grid_size - 1, F_a, F_b).pdf()) + G_probs = jnp.array(BetaBinomial(grid_size - 1, G_a, G_b).pdf()) + + return CareerWorkerProblem(β=β, θ=θ, ϵ=ϵ, + F_probs=F_probs, G_probs=G_probs, + F_mean=θ @ F_probs, G_mean=ϵ @ G_probs) ``` -The following function takes an instance of `CareerWorkerProblem` and returns -the corresponding Bellman operator $T$ and the greedy policy function. - -In this model, $T$ is defined by $Tv(\theta, \epsilon) = \max\{I, II, III\}$, where +The Bellman operator is $Tv(\theta, \epsilon) = \max\{I, II, III\}$, where $I$, $II$ and $III$ are as given in {eq}`eyes`. -```{code-cell} python3 -def operator_factory(cw, parallel_flag=True): +We start by writing those three values for a **single** state +$(\theta_i, \epsilon_j)$, so that the code sits close to the equation. +```{code-cell} ipython3 +def _B(v, cw, i, j): """ - Returns jitted versions of the Bellman operator and the - greedy policy function - - cw is an instance of ``CareerWorkerProblem`` + The values of the three options available at state (θ_i, ϵ_j), in the + order they appear in the Bellman equation. """ + stay_put = cw.θ[i] + cw.ϵ[j] + cw.β * v[i, j] # I + new_job = cw.θ[i] + cw.G_mean + cw.β * v[i, :] @ cw.G_probs # II + new_life = cw.G_mean + cw.F_mean + cw.β * cw.F_probs @ v @ cw.G_probs # III + return jnp.array([stay_put, new_job, new_life]) +``` - θ, ϵ, β = cw.θ, cw.ϵ, cw.β - F_probs, G_probs = cw.F_probs, cw.G_probs - F_mean, G_mean = cw.F_mean, cw.G_mean +Now we evaluate `_B` at every state. - @jit(parallel=parallel_flag) - def T(v): - "The Bellman operator" +Rather than write two nested loops over $i$ and $j$, we apply `jax.vmap` twice. - v_new = np.empty_like(v) +In `in_axes`, a `0` marks the argument being mapped over, while `None` holds an +argument fixed. - for i in prange(len(v)): - for j in prange(len(v)): - v1 = θ[i] + ϵ[j] + β * v[i, j] # Stay put - v2 = θ[i] + G_mean + β * v[i, :] @ G_probs # New job - v3 = G_mean + F_mean + β * F_probs @ v @ G_probs # New life - v_new[i, j] = max(v1, v2, v3) +```{code-cell} ipython3 +# The argument order of _B is (v, cw, i, j) +_B_j = jax.vmap(_B, in_axes=(None, None, None, 0)) # over j +_B_ij = jax.vmap(_B_j, in_axes=(None, None, 0, None)) # then over i - return v_new - @jit - def get_greedy(v): - "Computes the v-greedy policy" +@jax.jit +def B(v, cw): + "Value of each option at each state; shape (grid_size, grid_size, 3)." + n = len(cw.θ) + return _B_ij(v, cw, jnp.arange(n), jnp.arange(n)) +``` - σ = np.empty(v.shape) +The Bellman operator and the greedy policy are now the maximum and the +maximizer of the same array. - for i in range(len(v)): - for j in range(len(v)): - v1 = θ[i] + ϵ[j] + β * v[i, j] - v2 = θ[i] + G_mean + β * v[i, :] @ G_probs - v3 = G_mean + F_mean + β * F_probs @ v @ G_probs - if v1 > max(v2, v3): - action = 1 - elif v2 > max(v1, v3): - action = 2 - else: - action = 3 - σ[i, j] = action +```{code-cell} ipython3 +@jax.jit +def T(v, cw): + "The Bellman operator." + return jnp.max(B(v, cw), axis=-1) - return σ - return T, get_greedy +@jax.jit +def get_greedy(v, cw): + "The v-greedy policy, coded as 1 = stay put, 2 = new job, 3 = new life." + return jnp.argmax(B(v, cw), axis=-1) + 1 ``` -Lastly, `solve_model` will take an instance of `CareerWorkerProblem` and -iterate using the Bellman operator to find the fixed point of the Bellman equation. - -```{code-cell} python3 -def solve_model(cw, - use_parallel=True, - tol=1e-4, - max_iter=1000, - verbose=True, - print_skip=25): +Lastly, `solve_model` iterates the Bellman operator to find the fixed point. - T, _ = operator_factory(cw, parallel_flag=use_parallel) +We use `jax.lax.while_loop` so that the whole iteration compiles into a single +operation, and bound the number of steps so the loop always terminates. - # Set up loop - v = np.full((cw.grid_size, cw.grid_size), 100.) # Initial guess - i = 0 - error = tol + 1 +```{code-cell} ipython3 +@jax.jit +def solve_model(cw, tol=1e-4, max_iter=1_000): + """ + Solve the model by value function iteration. - while i < max_iter and error > tol: - v_new = T(v) - error = np.max(np.abs(v - v_new)) - i += 1 - if verbose and i % print_skip == 0: - print(f"Error at iteration {i} is {error}.") - v = v_new + Returns the value function, the number of iterations taken and the final + error, so that the caller can check convergence. + """ + def condition(loop_state): + i, v, error = loop_state + return (error > tol) & (i < max_iter) + + def update(loop_state): + i, v, error = loop_state + v_new = T(v, cw) + return i + 1, v_new, jnp.max(jnp.abs(v_new - v)) + + n = len(cw.θ) + v_init = jnp.full((n, n), 100.0) + i, v, error = jax.lax.while_loop(condition, update, (0, v_init, tol + 1)) + return v, i, error +``` - if error > tol: - print("Failed to converge!") +```{note} +The grid here is small, and this model would also run perfectly well in NumPy. - elif verbose: - print(f"\nConverged in {i} iterations.") +We use JAX because the code is almost as readable as the NumPy equivalent while +scaling far better --- to finer grids, or to richer versions of the model with +more state variables, where the same code will make full use of a GPU. - return v_new +The gain is already visible in {ref}`career_ex2`, where we simulate 25,000 +independent careers at once. ``` Here's the solution to the model -- an approximate value function -```{code-cell} python3 -cw = CareerWorkerProblem() -T, get_greedy = operator_factory(cw) -v_star = solve_model(cw, verbose=False) -greedy_star = get_greedy(v_star) +```{code-cell} ipython3 +cw = create_career_worker_problem() +v_star, num_iter, error = solve_model(cw) +greedy_star = get_greedy(v_star, cw) + +print(f"Converged in {num_iter} iterations with error {error:.2e}.") fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection='3d') -tg, eg = np.meshgrid(cw.θ, cw.ϵ) +tg, eg = jnp.meshgrid(cw.θ, cw.ϵ) ax.plot_surface(tg, eg, v_star.T, @@ -331,9 +333,9 @@ plt.show() And here is the optimal policy -```{code-cell} python3 +```{code-cell} ipython3 fig, ax = plt.subplots(figsize=(6, 6)) -tg, eg = np.meshgrid(cw.θ, cw.ϵ) +tg, eg = jnp.meshgrid(cw.θ, cw.ϵ) lvls = (0.5, 1.5, 2.5, 3.5) ax.contourf(tg, eg, greedy_star.T, levels=lvls, cmap=cm.winter, alpha=0.5) ax.contour(tg, eg, greedy_star.T, colors='k', levels=lvls, linewidths=2) @@ -352,8 +354,7 @@ Interpretation: Notice that the worker will always hold on to a sufficiently good career, but not necessarily hold on to even the best paying job. -The reason is that high lifetime wages require both variables to be large, and -the worker cannot change careers without changing jobs. +The reason is that high lifetime wages require both a good job and a good career, but the worker cannot change careers without changing jobs. * Sometimes a good job must be sacrificed in order to change to a better career. @@ -363,7 +364,7 @@ the worker cannot change careers without changing jobs. :label: career_ex1 ``` -Using the default parameterization in the class `CareerWorkerProblem`, +Using the default parameterization in the function `create_career_worker_problem`, generate and plot typical sample paths for $\theta$ and $\epsilon$ when the worker follows the optimal policy. @@ -375,7 +376,7 @@ In particular, modulo randomness, reproduce the following figure (where the hori ```{hint} :class: dropdown -To generate the draws from the distributions $F$ and $G$, use `quantecon.random.draw()`. +To draw from $F$ and $G$, invert their cdfs with `jnp.searchsorted`. ``` ```{exercise-end} @@ -388,51 +389,57 @@ To generate the draws from the distributions $F$ and $G$, use `quantecon.random. Simulate job/career paths. -In reading the code, recall that `optimal_policy[i, j]` = policy at +In reading the code, recall that `greedy_star[i, j]` = policy at $(\theta_i, \epsilon_j)$ = either 1, 2 or 3; meaning 'stay put', 'new job' and 'new life'. -```{code-cell} python3 -F = np.cumsum(cw.F_probs) -G = np.cumsum(cw.G_probs) -v_star = solve_model(cw, verbose=False) -T, get_greedy = operator_factory(cw) -greedy_star = get_greedy(v_star) +```{code-cell} ipython3 +def draw(key, cdf): + "Draw an index from the distribution with the given cdf." + return jnp.searchsorted(cdf, jr.uniform(key), side="right") + + +def simulate_path(cw, greedy_star, key, t=20): + "Simulate a career/job path of length t under the greedy policy." + F_cdf, G_cdf = jnp.cumsum(cw.F_probs), jnp.cumsum(cw.G_probs) -def gen_path(optimal_policy, F, G, t=20): - i = j = 0 - θ_index = [] - ϵ_index = [] - for t in range(t): - if optimal_policy[i, j] == 1: # Stay put - pass + def update(state, key): + i, j = state + action = greedy_star[i, j] + key_F, key_G = jr.split(key) + # Career changes only under 'new life'; the job changes unless we stay put + i_new = jnp.where(action == 3, draw(key_F, F_cdf), i) + j_new = jnp.where(action == 1, j, draw(key_G, G_cdf)) + return (i_new, j_new), (i_new, j_new) - elif greedy_star[i, j] == 2: # New job - j = qe.random.draw(G) + _, (i_path, j_path) = jax.lax.scan(update, (0, 0), jr.split(key, t)) + return cw.θ[i_path], cw.ϵ[j_path] - else: # New life - i, j = qe.random.draw(F), qe.random.draw(G) - θ_index.append(i) - ϵ_index.append(j) - return cw.θ[θ_index], cw.ϵ[ϵ_index] +cw = create_career_worker_problem() +v_star, _, _ = solve_model(cw) +greedy_star = get_greedy(v_star, cw) +key = jr.key(42) fig, axes = plt.subplots(2, 1, figsize=(10, 8)) + for ax in axes: - θ_path, ϵ_path = gen_path(greedy_star, F, G) + key, subkey = jr.split(key) + θ_path, ϵ_path = simulate_path(cw, greedy_star, subkey) ax.plot(ϵ_path, label='ϵ') ax.plot(θ_path, label='θ') ax.set_ylim(0, 6) + ax.legend() -plt.legend() plt.show() ``` ```{solution-end} ``` -```{exercise} +```{exercise-start} :label: career_ex2 +``` Let's now consider how long it takes for the worker to settle down to a permanent job, given a starting point of $(\theta, \epsilon) = (0, 0)$. @@ -456,48 +463,61 @@ $$ Collect 25,000 draws of this random variable and compute the median (which should be about 7). Repeat the exercise with $\beta=0.99$ and interpret the change. + +```{exercise-end} ``` ```{solution-start} career_ex2 :class: dropdown ``` -The median for the original parameterization can be computed as follows +The median for the original parameterization can be computed as follows. + +Each simulation is an independent sequential search, so we write one with +`jax.lax.while_loop` and then run 25,000 of them at once with `jax.vmap`. + +```{code-cell} ipython3 +def passage_time(cw, greedy_star, key, max_t=1_000): + "Time until the worker first chooses to stay put." + F_cdf, G_cdf = jnp.cumsum(cw.F_probs), jnp.cumsum(cw.G_probs) + + def condition(state): + i, j, t, key = state + return (greedy_star[i, j] != 1) & (t < max_t) + + def update(state): + i, j, t, key = state + action = greedy_star[i, j] + key, key_F, key_G = jr.split(key, 3) + i_new = jnp.where(action == 3, draw(key_F, F_cdf), i) + j_new = jnp.where(action == 1, j, draw(key_G, G_cdf)) + return i_new, j_new, t + 1, key -```{code-cell} python3 -cw = CareerWorkerProblem() -F = np.cumsum(cw.F_probs) -G = np.cumsum(cw.G_probs) -T, get_greedy = operator_factory(cw) -v_star = solve_model(cw, verbose=False) -greedy_star = get_greedy(v_star) + _, _, t, _ = jax.lax.while_loop(condition, update, (0, 0, 0, key)) + return t -@jit -def passage_time(optimal_policy, F, G): - t = 0 - i = j = 0 - while True: - if optimal_policy[i, j] == 1: # Stay put - return t - elif optimal_policy[i, j] == 2: # New job - j = qe.random.draw(G) - else: # New life - i, j = qe.random.draw(F), qe.random.draw(G) - t += 1 -@jit(parallel=True) -def median_time(optimal_policy, F, G, M=25000): - samples = np.empty(M) - for i in prange(M): - samples[i] = passage_time(optimal_policy, F, G) - return np.median(samples) +@jax.jit +def median_passage_time(cw, greedy_star, key, M=25_000): + "Median time to settle down, over M independent simulations." + keys = jr.split(key, M) + times = jax.vmap(passage_time, in_axes=(None, None, 0))(cw, greedy_star, keys) + return jnp.median(times) -median_time(greedy_star, F, G) + +median_passage_time(cw, greedy_star, jr.key(42)) ``` To compute the median with $\beta=0.99$ instead of the default -value $\beta=0.95$, replace `cw = CareerWorkerProblem()` with -`cw = CareerWorkerProblem(β=0.99)`. +value $\beta=0.95$, we create a new instance and solve it again. + +```{code-cell} ipython3 +cw_patient = create_career_worker_problem(β=0.99) +v_patient, _, _ = solve_model(cw_patient) +greedy_patient = get_greedy(v_patient, cw_patient) + +median_passage_time(cw_patient, greedy_patient, jr.key(42)) +``` The medians are subject to randomness but should be about 7 and 14 respectively. @@ -506,7 +526,6 @@ Not surprisingly, more patient workers will wait longer to settle down to their ```{solution-end} ``` - ```{exercise} :label: career_ex3 @@ -520,14 +539,13 @@ figure -- interpret. Here is one solution -```{code-cell} python3 -cw = CareerWorkerProblem(G_a=100, G_b=100) -T, get_greedy = operator_factory(cw) -v_star = solve_model(cw, verbose=False) -greedy_star = get_greedy(v_star) +```{code-cell} ipython3 +cw = create_career_worker_problem(G_a=100, G_b=100) +v_star, _, _ = solve_model(cw) +greedy_star = get_greedy(v_star, cw) fig, ax = plt.subplots(figsize=(6, 6)) -tg, eg = np.meshgrid(cw.θ, cw.ϵ) +tg, eg = jnp.meshgrid(cw.θ, cw.ϵ) lvls = (0.5, 1.5, 2.5, 3.5) ax.contourf(tg, eg, greedy_star.T, levels=lvls, cmap=cm.winter, alpha=0.5) ax.contour(tg, eg, greedy_star.T, colors='k', levels=lvls, linewidths=2) diff --git a/lectures/ifp_advanced.md b/lectures/ifp_advanced.md index 623db39..71a157f 100644 --- a/lectures/ifp_advanced.md +++ b/lectures/ifp_advanced.md @@ -32,7 +32,7 @@ In addition to what's in Anaconda, this lecture will need the following librarie --- tags: [hide-output] --- -!pip install quantecon +!pip install quantecon jax ``` ## Overview @@ -356,7 +356,7 @@ def create_ifp( assert β * ER < 1, "Stability condition failed." # Generate random draws using JAX - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) subkey1, subkey2 = jax.random.split(key) η_draws = jax.random.normal(subkey1, (shock_draw_size,)) ζ_draws = jax.random.normal(subkey2, (shock_draw_size,)) @@ -388,8 +388,8 @@ Here's the Coleman-Reffett operator using JAX: ```{code-cell} ipython3 def K( - a_in: jnp.array, # a_in[i, z] is an asset grid c_in: jnp.array, # c_in[i, z] = consumption at a_in[i, z] + a_in: jnp.array, # a_in[i, z] is an asset grid ifp: IFP ): """ @@ -430,7 +430,7 @@ def K( c_out = c_out.at[0, :].set(0) a_out = a_out.at[0, :].set(0) - return a_out, c_out + return c_out, a_out ``` The next function solves for an approximation of the optimal consumption policy @@ -487,15 +487,15 @@ a_init = σ_init.copy() Let's generate an approximation solution with JAX: ```{code-cell} ipython3 -a_star, σ_star = solve_model(ifp, a_init, σ_init) +σ_star, a_star = solve_model(ifp, σ_init, a_init) ``` Let's try it again with a timer. ```{code-cell} python3 with qe.Timer(precision=8): - a_star, σ_star = solve_model(ifp, a_init, σ_init) - a_star.block_until_ready() + σ_star, a_star = solve_model(ifp, σ_init, a_init) + σ_star.block_until_ready() ``` ## Simulation @@ -586,7 +586,7 @@ def compute_asset_stationary( z_idx_0_vector = jnp.zeros(num_households).astype(jnp.int32) # Vectorize over many households - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) keys = jax.random.split(key, num_households) # Vectorize simulate_household in (key, a_0, z_idx_0) sim_all_households = jax.vmap( @@ -642,7 +642,7 @@ s_grid = ifp.s_grid n_z = len(ifp.P) a_init = s_grid[:, None] * jnp.ones(n_z) c_init = a_init -a_vec, c_vec = solve_model(ifp, a_init, c_init) +c_vec, a_vec = solve_model(ifp, c_init, a_init) assets = compute_asset_stationary(c_vec, a_vec, ifp, num_households=200_000) # Compute Gini coefficient for the plot @@ -734,8 +734,8 @@ for a_r in a_r_vals: n_z_temp = len(ifp_temp.P) a_init_temp = s_grid_temp[:, None] * jnp.ones(n_z_temp) c_init_temp = a_init_temp - a_vec_temp, c_vec_temp = solve_model( - ifp_temp, a_init_temp, c_init_temp + c_vec_temp, a_vec_temp = solve_model( + ifp_temp, c_init_temp, a_init_temp ) # Simulate households @@ -811,8 +811,8 @@ for a_y in a_y_vals: n_z_temp = len(ifp_temp.P) a_init_temp = s_grid_temp[:, None] * jnp.ones(n_z_temp) c_init_temp = a_init_temp - a_vec_temp, c_vec_temp = solve_model( - ifp_temp, a_init_temp, c_init_temp + c_vec_temp, a_vec_temp = solve_model( + ifp_temp, c_init_temp, a_init_temp ) # Simulate households diff --git a/lectures/ifp_egm.md b/lectures/ifp_egm.md index 182e100..9178a8b 100644 --- a/lectures/ifp_egm.md +++ b/lectures/ifp_egm.md @@ -890,7 +890,7 @@ def compute_asset_stationary( z_idx_0_vector = jnp.zeros(num_households).astype(jnp.int32) # Vectorize over many households - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) keys = jax.random.split(key, num_households) # Vectorize simulate_household in (key, a_0, z_idx_0) sim_all_households = jax.vmap( diff --git a/lectures/ifp_egm_transient_shocks.md b/lectures/ifp_egm_transient_shocks.md index 4941525..8ff9008 100644 --- a/lectures/ifp_egm_transient_shocks.md +++ b/lectures/ifp_egm_transient_shocks.md @@ -411,7 +411,7 @@ def create_ifp(r=0.01, shock_draw_size=100, seed=1234): - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) s = jnp.linspace(0, savings_grid_max, savings_grid_size) Π, z_grid = jnp.array(Π), jnp.array(z_grid) R = 1 + r @@ -779,7 +779,7 @@ def compute_asset_stationary( z_idx_0_vector = jnp.zeros(num_households).astype(jnp.int32) # Vectorize over many households - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) keys = jax.random.split(key, num_households) # Vectorize simulate_household in (key, a_0, z_idx_0) sim_all_households = jax.vmap( diff --git a/lectures/inventory_q.md b/lectures/inventory_q.md index 9095b57..5e798f6 100644 --- a/lectures/inventory_q.md +++ b/lectures/inventory_q.md @@ -355,13 +355,12 @@ At each step, we draw a demand shock from the geometric distribution and update ```{code-cell} ipython3 @numba.jit(nopython=True) -def sim_inventories(ts_length, σ, p, X_init=0, seed=0): +def sim_inventories(ts_length, σ, p, rng, X_init=0): """Simulate inventory dynamics under policy σ.""" - np.random.seed(seed) X = np.zeros(ts_length, dtype=np.int32) X[0] = X_init for t in range(ts_length - 1): - d = np.random.geometric(p) - 1 + d = rng.geometric(p) - 1 X[t+1] = max(X[t] - d, 0) + σ[X[t]] return X ``` @@ -373,8 +372,8 @@ a large order to replenish stock (the upward jumps), after which inventory gradually declines as demand is served. ```{code-cell} ipython3 -def plot_ts(ts_length=200, fontsize=10): - X = sim_inventories(ts_length, σ_star, p) +def plot_ts(ts_length=200, fontsize=10, seed=0): + X = sim_inventories(ts_length, σ_star, p, np.random.default_rng(seed)) fig, ax = plt.subplots() ax.plot(X, label=r"$X_t$", alpha=0.7) @@ -588,8 +587,7 @@ At specified step counts (given by `snapshot_steps`), we record the current gree ```{code-cell} ipython3 @numba.jit(nopython=True) def q_learning_kernel(K, p, c, κ, β, n_steps, X_init, - ε_init, ε_min, ε_decay, q_init, snapshot_steps, seed): - np.random.seed(seed) + ε_init, ε_min, ε_decay, q_init, snapshot_steps, rng): q = np.full((K + 1, K + 1), q_init) n = np.zeros((K + 1, K + 1)) # visit counts for learning rate ε = ε_init @@ -600,7 +598,7 @@ def q_learning_kernel(K, p, c, κ, β, n_steps, X_init, # Initialize state and action x = X_init - a = np.random.randint(0, K - x + 1) + a = rng.integers(0, K - x + 1) for t in range(n_steps): # Record policy snapshot if needed @@ -609,7 +607,7 @@ def q_learning_kernel(K, p, c, κ, β, n_steps, X_init, snap_idx += 1 # === Draw D_{t+1} and observe outcome === - d = np.random.geometric(p) - 1 + d = rng.geometric(p) - 1 reward = min(x, d) - c * a - κ * (a > 0) x_next = max(x - d, 0) + a @@ -629,8 +627,8 @@ def q_learning_kernel(K, p, c, κ, β, n_steps, X_init, # === Behavior policy: ε-greedy (uses a_next, the argmax action) === x = x_next - if np.random.random() < ε: - a = np.random.randint(0, K - x + 1) + if rng.random() < ε: + a = rng.integers(0, K - x + 1) else: a = a_next ε = max(ε_min, ε * ε_decay) @@ -648,8 +646,9 @@ def q_learning(model, n_steps=20_000_000, X_init=0, K = len(x_values) - 1 if snapshot_steps is None: snapshot_steps = np.array([], dtype=np.int64) + rng = np.random.default_rng(seed) return q_learning_kernel(K, p, c, κ, β, n_steps, X_init, - ε_init, ε_min, ε_decay, q_init, snapshot_steps, seed) + ε_init, ε_min, ε_decay, q_init, snapshot_steps, rng) ``` Next we run $n$ = 5 million steps and take policy snapshots at steps 10,000, 1,000,000, and $n$. @@ -726,7 +725,8 @@ X_init = K // 2 sim_seed = 5678 # Optimal policy -X_opt = sim_inventories(ts_length, σ_star, p, X_init, seed=sim_seed) +X_opt = sim_inventories(ts_length, σ_star, p, + np.random.default_rng(sim_seed), X_init) axes[0].plot(X_opt, alpha=0.7) axes[0].set_ylabel("inventory") axes[0].set_title("Optimal (VFI)") @@ -735,7 +735,8 @@ axes[0].set_ylim(0, K + 2) # Q-learning snapshots for i in range(n_snaps): σ_snap = snapshots[i] - X = sim_inventories(ts_length, σ_snap, p, X_init, seed=sim_seed) + X = sim_inventories(ts_length, σ_snap, p, + np.random.default_rng(sim_seed), X_init) axes[i + 1].plot(X, alpha=0.7) axes[i + 1].set_ylabel("inventory") axes[i + 1].set_title(f"Step {snap_steps[i]:,}") diff --git a/lectures/jv.md b/lectures/jv.md index bd41ece..691acec 100644 --- a/lectures/jv.md +++ b/lectures/jv.md @@ -18,7 +18,7 @@ kernelspec: ``` -# {index}`Job Search VII: On-the-Job Search ` +# {index}`Job Search VIII: On-the-Job Search ` ```{index} single: Models; On-the-Job Search ``` @@ -27,6 +27,17 @@ kernelspec: :depth: 2 ``` +```{include} _static/_shared/_admonition/gpu.md +``` + +In addition to what's in Anaconda, this lecture will need the following libraries: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install jax +``` + ## Overview In this section, we solve a simple on-the-job search model @@ -35,11 +46,14 @@ In this section, we solve a simple on-the-job search model Let's start with some imports: -```{code-cell} ipython +```{code-cell} ipython3 +from typing import NamedTuple + import matplotlib.pyplot as plt -import numpy as np import scipy.stats as stats -from numba import jit, prange +import jax +import jax.numpy as jnp +import jax.random as jr ``` ### Model Features @@ -174,186 +188,193 @@ Now let's turn to implementation, and see if we can match our predictions. ```{index} single: On-the-Job Search; Programming Implementation ``` -We will set up a class `JVWorker` that holds the parameters of the model described above - -```{code-cell} python3 -class JVWorker: - r""" - A Jovanovic-type model of employment with on-the-job search. - +We solve the model with [JAX](https://docs.jax.dev/), using a `NamedTuple` to +hold the parameters and grids. + +```{code-cell} ipython3 +class JVWorker(NamedTuple): + A: float # Scale parameter in g + α: float # Curvature parameter in g + β: float # Discount factor + x_grid: jnp.ndarray # Grid of human capital values + s_grid: jnp.ndarray # Grid of search effort values + ϕ_grid: jnp.ndarray # Grid of investment values + f_rvs: jnp.ndarray # Draws from f, for Monte Carlo integration + + +def create_jv_worker(A=1.4, # Scale parameter in g + α=0.6, # Curvature parameter in g + β=0.96, # Discount factor + a=2, # Parameter of f + b=2, # Parameter of f + grid_size=50, # Size of the state grid + mc_size=100, # Number of draws from f + search_grid_size=15, # Size of each action grid + ɛ=1e-4, + seed=1234): + """ + Create an instance of the on-the-job search model. """ + f_rvs = jr.beta(jr.key(seed), a, b, (mc_size,)) - def __init__(self, - A=1.4, - α=0.6, - β=0.96, # Discount factor - π=np.sqrt, # Search effort function - a=2, # Parameter of f - b=2, # Parameter of f - grid_size=50, - mc_size=100, - ɛ=1e-4): + # Max of grid is the max of a large quantile value for f and the + # fixed point y = g(y, 1) + grid_max = max(A**(1 / (1 - α)), stats.beta(a, b).ppf(1 - ɛ)) - self.A, self.α, self.β, self.π = A, α, β, π - self.mc_size, self.ɛ = mc_size, ɛ + x_grid = jnp.linspace(ɛ, grid_max, grid_size) + s_grid = jnp.linspace(ɛ, 1, search_grid_size) + ϕ_grid = jnp.linspace(ɛ, 1, search_grid_size) - self.g = jit(lambda x, ϕ: A * (x * ϕ)**α) # Transition function - self.f_rvs = np.random.beta(a, b, mc_size) + return JVWorker(A=A, α=α, β=β, x_grid=x_grid, s_grid=s_grid, + ϕ_grid=ϕ_grid, f_rvs=f_rvs) +``` - # Max of grid is the max of a large quantile value for f and the - # fixed point y = g(y, 1) - ɛ = 1e-4 - grid_max = max(A**(1 / (1 - α)), stats.beta(a, b).ppf(1 - ɛ)) +Here are the transition function $g$ and the offer probability $\pi$. - # Human capital - self.x_grid = np.linspace(ɛ, grid_max, grid_size) -``` +```{code-cell} ipython3 +@jax.jit +def g(jv, x, ϕ): + "Transition function for job-specific human capital." + return jv.A * (x * ϕ)**jv.α -The function `operator_factory` takes an instance of this class and returns a -jitted version of the Bellman operator `T`, i.e. -$$ -Tv(x) -= \max_{s + \phi \leq 1} w(s, \phi) -$$ +@jax.jit +def π(s): + "Probability of receiving an offer when search effort is s." + return jnp.sqrt(s) +``` -where +Next we write the right-hand side of the Bellman equation {eq}`jvbell`, before +maximization: ```{math} :label: defw -w(s, \phi) +B(x, s, \phi) := x (1 - s - \phi) + \beta (1 - \pi(s)) v[g(x, \phi)] + \beta \pi(s) \int v[g(x, \phi) \vee u] f(du) ``` -When we represent $v$, it will be with a NumPy array `v` giving values on grid `x_grid`. +We represent $v$ by an array giving its values on `x_grid`, and recover a +function from it by linear interpolation. -But to evaluate the right-hand side of {eq}`defw`, we need a function, so -we replace the arrays `v` and `x_grid` with a function `v_func` that gives linear -interpolation of `v` on `x_grid`. +The integral is replaced by a Monte Carlo average over the draws in `f_rvs`. -Inside the `for` loop, for each `x` in the grid over the state space, we -set up the function $w(z) = w(s, \phi)$ defined in {eq}`defw`. +The function below is written for a **single** state $x$ and a **single** +action pair $(s, \phi)$ --- so it reads much like {eq}`defw` itself. -The function is maximized over all feasible $(s, \phi)$ pairs. +```{code-cell} ipython3 +def _B(v, jv, x, s, ϕ): + """ + The right-hand side of the Bellman equation before maximization, for one + state x and one action pair (s, ϕ). -Another function, `get_greedy` returns the optimal choice of $s$ and $\phi$ -at each $x$, given a value function. + Infeasible pairs, where s + ϕ > 1, are given value -∞ so that they are + never selected by the maximization step. + """ + v_func = lambda z: jnp.interp(z, jv.x_grid, v) + gxϕ = g(jv, x, ϕ) -```{code-cell} python3 -def operator_factory(jv, parallel_flag=True): + # Monte Carlo estimate of ∫ v[g(x, ϕ) ∨ u] f(du) + integral = jnp.mean(v_func(jnp.maximum(gxϕ, jv.f_rvs))) - """ - Returns a jitted version of the Bellman operator T + q = π(s) * integral + (1 - π(s)) * v_func(gxϕ) + return jnp.where(s + ϕ <= 1, x * (1 - s - ϕ) + jv.β * q, -jnp.inf) +``` - jv is an instance of JVWorker +Now we evaluate `_B` at every combination of state and action. - """ +Rather than write three nested loops, we apply `jax.vmap` three times. - π, β = jv.π, jv.β - x_grid, ɛ, mc_size = jv.x_grid, jv.ɛ, jv.mc_size - f_rvs, g = jv.f_rvs, jv.g - - @jit - def state_action_values(z, x, v): - s, ϕ = z - v_func = lambda x: np.interp(x, x_grid, v) - - integral = 0 - for m in range(mc_size): - u = f_rvs[m] - integral += v_func(max(g(x, ϕ), u)) - integral = integral / mc_size - - q = π(s) * integral + (1 - π(s)) * v_func(g(x, ϕ)) - return x * (1 - ϕ - s) + β * q - - @jit(parallel=parallel_flag) - def T(v): - """ - The Bellman operator - """ - - v_new = np.empty_like(v) - for i in prange(len(x_grid)): - x = x_grid[i] - - # Search on a grid - search_grid = np.linspace(ɛ, 1, 15) - max_val = -1 - for s in search_grid: - for ϕ in search_grid: - current_val = state_action_values((s, ϕ), x, v) if s + ϕ <= 1 else -1 - if current_val > max_val: - max_val = current_val - v_new[i] = max_val - - return v_new - - @jit - def get_greedy(v): - """ - Computes the v-greedy policy of a given function v - """ - s_policy, ϕ_policy = np.empty_like(v), np.empty_like(v) - - for i in range(len(x_grid)): - x = x_grid[i] - # Search on a grid - search_grid = np.linspace(ɛ, 1, 15) - max_val = -1 - for s in search_grid: - for ϕ in search_grid: - current_val = state_action_values((s, ϕ), x, v) if s + ϕ <= 1 else -1 - if current_val > max_val: - max_val = current_val - max_s, max_ϕ = s, ϕ - s_policy[i], ϕ_policy[i] = max_s, max_ϕ - return s_policy, ϕ_policy - - return T, get_greedy -``` - -To solve the model, we will write a function that uses the Bellman operator -and iterates to find a fixed point. - -```{code-cell} python3 -def solve_model(jv, - use_parallel=True, - tol=1e-4, - max_iter=1000, - verbose=True, - print_skip=25): +Each application vectorizes over one argument, so the stack below plays the +role of a triple loop --- but the whole thing compiles to code that runs in +parallel. - """ - Solves the model by value function iteration +In `in_axes`, a `0` marks the argument being mapped over, while `None` holds an +argument fixed. + +```{code-cell} ipython3 +# The argument order of _B is (v, jv, x, s, ϕ) +_B_ϕ = jax.vmap(_B, in_axes=(None, None, None, None, 0)) # over ϕ +_B_sϕ = jax.vmap(_B_ϕ, in_axes=(None, None, None, 0, None)) # then over s +_B_xsϕ = jax.vmap(_B_sϕ, in_axes=(None, None, 0, None, None)) # then over x +``` - * jv is an instance of JVWorker +The result is a fully vectorized version of $B$. +```{code-cell} ipython3 +@jax.jit +def B(v, jv): """ + Evaluate B at every (state, action) combination. - T, _ = operator_factory(jv, parallel_flag=use_parallel) + Returns an array of shape (len(x_grid), len(s_grid), len(ϕ_grid)) where + entry [i, j, k] holds the value of choosing (s_j, ϕ_k) in state x_i. + """ + return _B_xsϕ(v, jv, jv.x_grid, jv.s_grid, jv.ϕ_grid) +``` - # Set up loop - v = jv.x_grid * 0.5 # Initial condition - i = 0 - error = tol + 1 +With `B` in hand, the Bellman operator and the greedy policy are both one-liners +--- we maximize over the two action axes, taking the maximum in one case and the +maximizer in the other. - while i < max_iter and error > tol: - v_new = T(v) - error = np.max(np.abs(v - v_new)) - i += 1 - if verbose and i % print_skip == 0: - print(f"Error at iteration {i} is {error}.") - v = v_new +```{code-cell} ipython3 +@jax.jit +def T(v, jv): + "The Bellman operator." + return jnp.max(B(v, jv), axis=(1, 2)) - if error > tol: - print("Failed to converge!") - elif verbose: - print(f"\nConverged in {i} iterations.") - return v_new +@jax.jit +def get_greedy(v, jv): + "Compute the v-greedy policy, returned as a pair (s_policy, ϕ_policy)." + vals = B(v, jv) + + # Flatten the two action axes so that a single argmax picks out the best + # pair at each state, then convert the flat index back to a (s, ϕ) pair + n_s, n_ϕ = len(jv.s_grid), len(jv.ϕ_grid) + best = jnp.argmax(vals.reshape(len(jv.x_grid), n_s * n_ϕ), axis=1) + j, k = jnp.unravel_index(best, (n_s, n_ϕ)) + + return jv.s_grid[j], jv.ϕ_grid[k] +``` + +To solve the model we iterate $T$ to convergence. + +We use `jax.lax.while_loop` so that the entire iteration compiles into a single +operation, and bound the number of steps so that the loop always terminates. + +```{code-cell} ipython3 +@jax.jit +def solve_model(jv, tol=1e-4, max_iter=1_000): + """ + Solve the model by value function iteration. + + Returns the value function, the number of iterations taken, and the final + error, so that the caller can check convergence. + """ + def condition(loop_state): + i, v, error = loop_state + return (error > tol) & (i < max_iter) + + def update(loop_state): + i, v, error = loop_state + v_new = T(v, jv) + return i + 1, v_new, jnp.max(jnp.abs(v_new - v)) + + v_init = jv.x_grid * 0.5 + i, v, error = jax.lax.while_loop(condition, update, (0, v_init, tol + 1)) + return v, i, error +``` + +```{note} +The grids here are small, and this model would also run perfectly well in +NumPy. + +We use JAX because the code is almost as readable as the NumPy equivalent, +while scaling far better --- to finer grids, or to richer versions of the model +with additional state variables, where the same code will make full use of a +GPU. ``` ## Solving for Policies @@ -364,16 +385,17 @@ def solve_model(jv, Let's generate the optimal policies and see what they look like. (jv_policies)= -```{code-cell} python3 -jv = JVWorker() -T, get_greedy = operator_factory(jv) -v_star = solve_model(jv) -s_star, ϕ_star = get_greedy(v_star) +```{code-cell} ipython3 +jv = create_jv_worker() +v_star, num_iter, error = solve_model(jv) +s_star, ϕ_star = get_greedy(v_star, jv) + +print(f"Converged in {num_iter} iterations with error {error:.2e}.") ``` Here are the plots: -```{code-cell} python3 +```{code-cell} ipython3 plots = [s_star, ϕ_star, v_star] titles = ["s policy", "ϕ policy", "value function"] @@ -418,10 +440,10 @@ x$. Plot this with one dot for each realization, in the form of a 45 degree diagram, setting -```{code-block} python3 -jv = JVWorker(grid_size=25, mc_size=50) +```{code-block} ipython3 +jv = create_jv_worker(grid_size=25, mc_size=50) plot_grid_max, plot_grid_size = 1.2, 100 -plot_grid = np.linspace(0, plot_grid_max, plot_grid_size) +plot_grid = jnp.linspace(0, plot_grid_max, plot_grid_size) fig, ax = plt.subplots() ax.set_xlim(0, plot_grid_max) ax.set_ylim(0, plot_grid_max) @@ -439,25 +461,42 @@ Argue that at the steady state, $s_t \approx 0$ and $\phi_t \approx 0.6$. :class: dropdown ``` -Here’s code to produce the 45 degree diagram +Here's code to produce the 45 degree diagram. + +Note that we draw all of the realizations at once, rather than looping over +states and draws. -```{code-cell} python3 -jv = JVWorker(grid_size=25, mc_size=50) -π, g, f_rvs, x_grid = jv.π, jv.g, jv.f_rvs, jv.x_grid -T, get_greedy = operator_factory(jv) -v_star = solve_model(jv, verbose=False) -s_policy, ϕ_policy = get_greedy(v_star) +```{code-cell} ipython3 +jv = create_jv_worker(grid_size=25, mc_size=50) +v_star, _, _ = solve_model(jv) +s_policy, ϕ_policy = get_greedy(v_star, jv) # Turn the policy function arrays into actual functions -s = lambda y: np.interp(y, x_grid, s_policy) -ϕ = lambda y: np.interp(y, x_grid, ϕ_policy) +s = lambda y: jnp.interp(y, jv.x_grid, s_policy) +ϕ = lambda y: jnp.interp(y, jv.x_grid, ϕ_policy) -def h(x, b, u): - return (1 - b) * g(x, ϕ(x)) + b * max(g(x, ϕ(x)), u) +plot_grid_max, plot_grid_size = 1.2, 100 +plot_grid = jnp.linspace(0, plot_grid_max, plot_grid_size) -plot_grid_max, plot_grid_size = 1.2, 100 -plot_grid = np.linspace(0, plot_grid_max, plot_grid_size) +@jax.jit +def simulate_next(key, plot_grid): + """ + Draw realizations of next period capital for every x in plot_grid, + following the law of motion for x_{t+1} given above. Returns an array of shape (len(plot_grid), mc_size). + """ + K = len(jv.f_rvs) + gxϕ = g(jv, plot_grid, ϕ(plot_grid))[:, jnp.newaxis] # Shape (n, 1) + u = jv.f_rvs[jnp.newaxis, :] # Shape (1, K) + + # An offer arrives with probability π(s(x)), independently across draws + b = jr.uniform(key, (len(plot_grid), K)) < π(s(plot_grid))[:, jnp.newaxis] + + return jnp.where(b, jnp.maximum(gxϕ, u), gxϕ) + + +x_next = simulate_next(jr.key(1234), plot_grid) + fig, ax = plt.subplots(figsize=(8, 8)) ticks = (0.25, 0.5, 0.75, 1.0) ax.set(xticks=ticks, yticks=ticks, @@ -466,12 +505,8 @@ ax.set(xticks=ticks, yticks=ticks, xlabel='$x_t$', ylabel='$x_{t+1}$') ax.plot(plot_grid, plot_grid, 'k--', alpha=0.6) # 45 degree line -for x in plot_grid: - for i in range(jv.mc_size): - b = 1 if np.random.uniform(0, 1) < π(s(x)) else 0 - u = f_rvs[i] - y = h(x, b, u) - ax.plot(x, y, 'go', alpha=0.25) +ax.plot(jnp.repeat(plot_grid, x_next.shape[1]), x_next.ravel(), + 'go', alpha=0.25) plt.show() ``` @@ -523,17 +558,17 @@ Can you give a rough interpretation for the value that you see? The figure can be produced as follows -```{code-cell} python3 -jv = JVWorker() +```{code-cell} ipython3 +jv = create_jv_worker() def xbar(ϕ): - A, α = jv.A, jv.α - return (A * ϕ**α)**(1 / (1 - α)) + return (jv.A * ϕ**jv.α)**(1 / (1 - jv.α)) + +ϕ_grid = jnp.linspace(0, 1, 100) -ϕ_grid = np.linspace(0, 1, 100) fig, ax = plt.subplots(figsize=(9, 7)) ax.set(xlabel=r'$\phi$') -ax.plot(ϕ_grid, [xbar(ϕ) * (1 - ϕ) for ϕ in ϕ_grid], label=r'$w^*(\phi)$') +ax.plot(ϕ_grid, xbar(ϕ_grid) * (1 - ϕ_grid), label=r'$w^*(\phi)$') ax.legend() plt.show() diff --git a/lectures/mccall_fitted_vfi.md b/lectures/mccall_fitted_vfi.md index 9dd644f..df8caa7 100644 --- a/lectures/mccall_fitted_vfi.md +++ b/lectures/mccall_fitted_vfi.md @@ -282,7 +282,7 @@ def create_mccall_model( ): """Factory function to create a McCall model instance.""" - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) z_draws = jax.random.normal(key, (mc_size,)) # Discretize just to get a suitable wage grid for interpolation @@ -529,7 +529,7 @@ def simulate_employment_path( Simulate employment path for T periods starting from unemployment. """ - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) c, α, β, ρ, ν, γ, w_grid, z_draws = model # Initial conditions: start unemployed with initial wage draw @@ -678,7 +678,7 @@ def simulate_cross_section( """ c, α, β, ρ, ν, γ, w_grid, z_draws = model - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) # Solve for optimal reservation wage w_bar = get_reservation_wage(model) @@ -750,7 +750,7 @@ def plot_cross_sectional_unemployment( c, α, β, ρ, ν, γ, w_grid, z_draws = model # Get final employment state directly - key = jax.random.PRNGKey(42) + key = jax.random.key(42) w_bar = get_reservation_wage(model) # Initialize arrays diff --git a/lectures/mccall_model.md b/lectures/mccall_model.md index 65b3041..df46a39 100644 --- a/lectures/mccall_model.md +++ b/lectures/mccall_model.md @@ -802,7 +802,7 @@ class McCallModelContinuous(NamedTuple): def create_mccall_continuous( c=25, β=0.99, σ=0.5, μ=2.5, mc_size=1000, seed=1234 ): - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) s = jax.random.normal(key, (mc_size,)) w_draws = jnp.exp(μ + σ * s) return McCallModelContinuous(c, β, σ, μ, w_draws) @@ -970,7 +970,7 @@ def simulate_lifetime_value(key, model, w_bar, n_periods=100): Parameters: ----------- - key : jax.random.PRNGKey + key : jax.random.key Random key for JAX model : McCallModelContinuous The model containing parameters @@ -1018,7 +1018,7 @@ def compute_mean_lifetime_value(model, w_bar, num_reps=10000, seed=1234): Compute mean lifetime value across many simulations. """ - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) keys = jax.random.split(key, num_reps) # Vectorize the simulation across all replications @@ -1096,7 +1096,7 @@ def compute_stopping_time_continuous(w_bar, key, model): ----------- w_bar : float The reservation wage - key : jax.random.PRNGKey + key : jax.random.key Random key for JAX model : McCallModelContinuous The model containing wage draws @@ -1148,7 +1148,7 @@ def compute_mean_stopping_time_continuous(w_bar, model, num_reps=100000, seed=12 Average stopping time across all replications """ # Generate a key for each MC replication - key = jax.random.PRNGKey(seed) + key = jax.random.key(seed) keys = jax.random.split(key, num_reps) # Vectorize compute_stopping_time_continuous and evaluate across keys diff --git a/lectures/mccall_persist_trans.md b/lectures/mccall_persist_trans.md index 03caa45..6f3a2a5 100644 --- a/lectures/mccall_persist_trans.md +++ b/lectures/mccall_persist_trans.md @@ -19,7 +19,7 @@ kernelspec: ``` -# Job Search V: Persistent and Transitory Wage Shocks +# Job Search VI: Persistent and Transitory Wage Shocks ```{include} _static/_shared/_admonition/gpu.md ``` @@ -192,7 +192,7 @@ class Model(NamedTuple): e_draws: jnp.ndarray def create_job_search_model(μ=0.0, s=1.0, d=0.0, ρ=0.9, σ=0.1, β=0.98, c=5.0, - mc_size=1000, grid_size=100, key=jax.random.PRNGKey(1234)): + mc_size=1000, grid_size=100, key=jax.random.key(1234)): """ Create a Model with computed grid and draws. """ @@ -377,7 +377,7 @@ def draw_duration(key, μ, s, d, ρ, σ, β, z_grid, f_star, t_max=10_000): def compute_unemployment_duration( - model, key=jax.random.PRNGKey(1234), num_reps=100_000 + model, key=jax.random.key(1234), num_reps=100_000 ): """ Compute expected unemployment duration. diff --git a/lectures/mccall_q.md b/lectures/mccall_q.md index 79950a0..dcefdcf 100644 --- a/lectures/mccall_q.md +++ b/lectures/mccall_q.md @@ -11,7 +11,7 @@ kernelspec: name: python3 --- -# Job Search IX: Search with Q-Learning +# Job Search X: Search with Q-Learning ## Overview @@ -25,7 +25,7 @@ The Q-learning algorithm combines ideas from * a recursive version of least squares known as [temporal difference learning](https://en.wikipedia.org/wiki/Temporal_difference_learning). -This lecture applies a Q-learning algorithm to the situation faced by a McCall worker. +This lecture applies a Q-learning algorithm to the situation faced by a McCall worker. This lecture also considers the case where a McCall worker is given an option to quit the current job. @@ -79,10 +79,10 @@ from quantecon.distributions import BetaBinomial import matplotlib.pyplot as plt -np.random.seed(123) +rng = np.random.default_rng(123) ``` -## Review of McCall Model +## Review of McCall model We begin by reviewing the McCall model described in {doc}`this quantecon lecture `. @@ -239,10 +239,10 @@ We'll use this value function as a benchmark later after we have done some Q-lea print(valfunc_VFI) ``` -## Implied Quality Function $Q$ +## Implied quality function $Q$ -A **quality function** $Q$ map state-action pairs into optimal values. +A **quality function** $Q$ maps state-action pairs into optimal values. They are tightly linked to optimal value functions. @@ -275,7 +275,7 @@ Q\left(w,\text{reject}\right) & =c+\beta\int\max_{\text{accept, reject}}\left\{ $$ (eq:impliedq) -Note that the first equation of system {eq}`eq:impliedq` presumes that after the agent has accepted an offer, he will not have the objection to reject that same offer in the future. +Note that the first equation of system {eq}`eq:impliedq` presumes that after the agent has accepted an offer, he will not have the option to reject that same offer in the future. These equations are aligned with the Bellman equation for the worker's optimal value function that we studied in {doc}`this quantecon lecture `. @@ -313,7 +313,7 @@ $$ +++ -## From Probabilities to Samples +## From probabilities to samples We noted above that the optimal Q function for our McCall worker satisfies the Bellman equations @@ -326,7 +326,7 @@ $$ (eq:probtosample1) Notice the integral over $F(w')$ on the second line. -Erasing the integral sign sets the stage for an illegitmate argument that can get us started thinking about Q-learning. +Erasing the integral sign sets the stage for an illegitimate argument that can get us started thinking about Q-learning. Thus, construct a difference equation system that keeps the first equation of {eq}`eq:probtosample1` but replaces the second by removing integration over $F (w')$: @@ -370,7 +370,7 @@ to objects in equation system {eq}`eq:old105`. This informal argument takes us to the threshold of Q-learning. -## Q-Learning +## Q-learning Let's first describe a $Q$-learning algorithm precisely. @@ -456,7 +456,7 @@ pseudo-code for our McCall worker to do Q-learning: 4. Update the state associated with the chosen action and compute $\widetilde{TD}$ according to {eq}`eq:old4` and update $\widetilde{Q}$ according to {eq}`eq:old3`. -5. Either draw a new state $w'$ if required or else take existing wage if and update the Q-table again according to {eq}`eq:old3`. +5. Either draw a new state $w'$ if required or else take the existing wage and update the Q-table again according to {eq}`eq:old3`. 6. Stop when the old and new Q-tables are close enough, i.e., $\lVert\tilde{Q}^{new}-\tilde{Q}^{old}\rVert_{\infty}\leq\delta$ for given $\delta$ or if the worker keeps accepting for $T$ periods for a prescribed $T$. @@ -474,7 +474,7 @@ The Q-table is updated via temporal difference learning. We iterate this until convergence of the Q-table or the maximum length of an episode is reached. -Multiple episodes allow the agent to start afresh and visit states that she was less likely to visit from the terminal state of a previos episode. +Multiple episodes allow the agent to start afresh and visit states that she was less likely to visit from the terminal state of a previous episode. For example, an agent who has accepted a wage offer based on her Q-table will be less likely to draw a new offer from other parts of the wage distribution. @@ -514,15 +514,15 @@ class Qlearning_McCall: self.quit_allowed = quit_allowed - def draw_offer_index(self): + def draw_offer_index(self, rng): """ Draw a state index from the wage distribution. """ q = self.q - return np.searchsorted(np.cumsum(q), np.random.random(), side="right") + return np.searchsorted(np.cumsum(q), rng.random(), side="right") - def temp_diff(self, qtable, state, accept): + def temp_diff(self, qtable, state, accept, rng): """ Compute the TD associated with state and action. """ @@ -530,7 +530,7 @@ class Qlearning_McCall: c, β, w = self.c, self.β, self.w if accept==0: - state_next = self.draw_offer_index() + state_next = self.draw_offer_index(rng) TD = c + β*np.max(qtable[state_next, :]) - qtable[state, accept] else: state_next = state @@ -541,7 +541,7 @@ class Qlearning_McCall: return TD, state_next - def run_one_epoch(self, qtable, max_times=20000): + def run_one_epoch(self, qtable, rng, max_times=20000): """ Run an "epoch". """ @@ -549,7 +549,7 @@ class Qlearning_McCall: c, β, w = self.c, self.β, self.w eps, δ, lr, T = self.eps, self.δ, self.lr, self.T - s0 = self.draw_offer_index() + s0 = self.draw_offer_index(rng) s = s0 accept_count = 0 @@ -557,7 +557,7 @@ class Qlearning_McCall: # choose action accept = np.argmax(qtable[s, :]) - if np.random.random()<=eps: + if rng.random()<=eps: accept = 1 - accept if accept == 1: @@ -565,7 +565,7 @@ class Qlearning_McCall: else: accept_count = 0 - TD, s_next = self.temp_diff(qtable, s, accept) + TD, s_next = self.temp_diff(qtable, s, accept, rng) # update qtable qtable_new = qtable.copy() @@ -582,15 +582,15 @@ class Qlearning_McCall: return qtable_new @jit -def run_epochs(N, qlmc, qtable): +def run_epochs(N, qlmc, qtable, rng): """ Run epochs N times with qtable from the last iteration each time. """ for n in range(N): - if n%(N/10)==0: + if n % max(1, N // 10) == 0: print(f"Progress: EPOCHs = {n}") - new_qtable = qlmc.run_one_epoch(qtable) + new_qtable = qlmc.run_one_epoch(qtable, rng) qtable = new_qtable return qtable @@ -608,7 +608,7 @@ qlmc = Qlearning_McCall() # run qtable0 = np.zeros((len(w_default), 2)) -qtable = run_epochs(20000, qlmc, qtable0) +qtable = run_epochs(20000, qlmc, qtable0, rng) ``` ```{code-cell} ipython3 @@ -651,10 +651,6 @@ ax.set_xlabel('wages') ax.set_ylabel('probabilities') plt.show() - -# VFI -mcm = McCallModel(w=w_new, q=q_new) -valfunc_VFI, flag = mcm.VFI() ``` ```{code-cell} ipython3 @@ -676,21 +672,23 @@ def plot_epochs(epochs_to_plot, quit_allowed=1): max_epochs = np.max(epochs_to_plot) # iterate on epoch numbers for n in range(max_epochs + 1): - if n%(max_epochs/10)==0: + if n % max(1, max_epochs // 10) == 0: print(f"Progress: EPOCHs = {n}") if n in epochs_to_plot: valfunc_qlr = valfunc_from_qtable(qtable) error = compute_error(valfunc_qlr, valfunc_VFI) - ax.plot(w_new, valfunc_qlr, '-o', label=f'QL:epochs={n}, mean error={error}') + ax.plot(w_new, valfunc_qlr, '-o', + label=f'QL: epochs={n}, mean error={error:.2f}') - new_qtable = qlmc_new.run_one_epoch(qtable) + new_qtable = qlmc_new.run_one_epoch(qtable, rng) qtable = new_qtable ax.set_xlabel('wages') ax.set_ylabel('optimal value') - ax.legend(loc='lower right') + ax.legend(bbox_to_anchor=(0.5, -0.15), loc='upper center', ncol=2) + plt.subplots_adjust(bottom=0.25) plt.show() ``` @@ -704,7 +702,7 @@ The above graphs indicates that * the quality of approximation to the "true" value function computed by value function iteration improves for longer epochs -## Employed Worker Can't Quit +## Employed worker can't quit The preceding version of temporal difference Q-learning described in equation system {eq}`eq:old4` lets an employed worker quit, i.e., reject her wage as an incumbent and instead receive unemployment compensation this period @@ -715,7 +713,7 @@ This is an option that the McCall worker described in {doc}`this quantecon lectu See {cite}`Ljungqvist2012`, chapter 6 on search, for a proof. But in the context of Q-learning, giving the worker the option to quit and get unemployment compensation while -unemployed turns out to accelerate the learning process by promoting experimentation vis a vis premature +unemployed turns out to accelerate the learning process by promoting experimentation versus premature exploitation only. To illustrate this, we'll amend our formulas for temporal differences to forbid an employed worker from quitting a job she had accepted earlier. @@ -731,7 +729,7 @@ $$ (eq:temp-diff) It turns out that formulas {eq}`eq:temp-diff` combined with our Q-learning recursion {eq}`eq:old3` can lead our agent to eventually learn the optimal value function as well as in the case where an option to redraw can be exercised. -But learning is slower because an agent who ends up accepting a wage offer prematurally loses the option to explore new states in the same episode and to adjust the value associated with that state. +But learning is slower because an agent who ends up accepting a wage offer prematurely loses the option to explore new states in the same episode and to adjust the value associated with that state. This can lead to inferior outcomes when the number of epochs/episodes is low. @@ -744,9 +742,9 @@ We illustrate these possibilities with the following code and graph. plot_epochs(epochs_to_plot=[100, 1000, 10000, 100000, 200000], quit_allowed=0) ``` -## Possible Extensions +## Possible extensions -To extend the algorthm to handle problems with continuous state spaces, +To extend the algorithm to handle problems with continuous state spaces, a typical approach is to restrict Q-functions and policy functions to take particular functional forms. diff --git a/lectures/opt_tax_recur.md b/lectures/opt_tax_recur.md index 69d1adb..87ebc72 100644 --- a/lectures/opt_tax_recur.md +++ b/lectures/opt_tax_recur.md @@ -31,7 +31,7 @@ tags: [hide-output] ## Overview -This lecture describes special case of a celebrated model of optimal fiscal policy by Robert E. +This lecture describes a special case of a celebrated model of optimal fiscal policy by Robert E. Lucas, Jr., and Nancy Stokey {cite}`LucasStokey1983`. @@ -43,7 +43,7 @@ The model features * a linear production function mapping labor into a single good * a representative household that likes both consumption and leisure -* an exogenous history-contingent sequence of government expenditures that a goverment must finance with revenues from a sequence of history-dependent flat rate taxes +* an exogenous history-contingent sequence of government expenditures that a government must finance with revenues from a sequence of history-dependent flat rate taxes * an exogenous initial debt the government must also finance * a Ramsey planner who at time $t=0$ chooses a history contingent plan for flat rate taxes at all $t \geq 0$ * a sequence of continuation governments that at each $t \geq 1$ must pay off the one-period state-contingent debt that a time $t-1$ government has issued diff --git a/lectures/os_stochastic.md b/lectures/os_stochastic.md index 204568e..5ba75f0 100644 --- a/lectures/os_stochastic.md +++ b/lectures/os_stochastic.md @@ -505,8 +505,8 @@ def create_model( x_grid = np.linspace(1e-4, grid_max, grid_size) # Store shocks (with a seed, so results are reproducible) - np.random.seed(seed) - shocks = np.exp(μ + ν * np.random.randn(shock_size)) + rng = np.random.default_rng(seed) + shocks = np.exp(μ + ν * rng.standard_normal(shock_size)) return Model(u, f, β, μ, ν, x_grid, shocks) ``` diff --git a/lectures/os_time_iter.md b/lectures/os_time_iter.md index f2205e7..815e13c 100644 --- a/lectures/os_time_iter.md +++ b/lectures/os_time_iter.md @@ -340,8 +340,8 @@ def create_model( grid = np.linspace(1e-4, grid_max, grid_size) # Store shocks (with a seed, so results are reproducible) - np.random.seed(seed) - shocks = np.exp(μ + ν * np.random.randn(shock_size)) + rng = np.random.default_rng(seed) + shocks = np.exp(μ + ν * rng.standard_normal(shock_size)) return Model(u, f, β, μ, ν, grid, shocks, α, u_prime, f_prime) ``` diff --git a/lectures/rs_inventory_q.md b/lectures/rs_inventory_q.md index 0a65a44..fdf4484 100644 --- a/lectures/rs_inventory_q.md +++ b/lectures/rs_inventory_q.md @@ -332,13 +332,12 @@ We simulate inventory dynamics under the optimal policy for the baseline $\gamma ```{code-cell} ipython3 @numba.jit(nopython=True) -def sim_inventories(ts_length, σ, p, X_init=0, seed=0): +def sim_inventories(ts_length, σ, p, rng, X_init=0): """Simulate inventory dynamics under policy σ.""" - np.random.seed(seed) X = np.zeros(ts_length, dtype=np.int32) X[0] = X_init for t in range(ts_length - 1): - d = np.random.geometric(p) - 1 + d = rng.geometric(p) - 1 X[t+1] = max(X[t] - d, 0) + σ[X[t]] return X ``` @@ -354,7 +353,8 @@ K = len(x_values) - 1 for i, γ in enumerate(γ_values): v, σ = results[γ] - X = sim_inventories(ts_length, σ, model.p, X_init=K // 2, seed=sim_seed) + X = sim_inventories(ts_length, σ, model.p, + np.random.default_rng(sim_seed), X_init=K // 2) axes[i].plot(X, alpha=0.7) axes[i].set_ylabel("inventory") axes[i].set_title(f"$\\gamma = {γ}$") @@ -588,8 +588,7 @@ the update target uses $\exp(-\gamma R_{t+1}) ```{code-cell} ipython3 @numba.jit(nopython=True) def q_learning_rs_kernel(K, p, c, κ, β, γ, n_steps, X_init, - ε_init, ε_min, ε_decay, q_init, snapshot_steps, seed): - np.random.seed(seed) + ε_init, ε_min, ε_decay, q_init, snapshot_steps, rng): q = np.full((K + 1, K + 1), q_init) # optimistic initialization n = np.zeros((K + 1, K + 1)) # visit counts for learning rate ε = ε_init @@ -600,7 +599,7 @@ def q_learning_rs_kernel(K, p, c, κ, β, γ, n_steps, X_init, # Initialize state and action x = X_init - a = np.random.randint(0, K - x + 1) + a = rng.integers(0, K - x + 1) for t in range(n_steps): # Record policy snapshot if needed @@ -609,7 +608,7 @@ def q_learning_rs_kernel(K, p, c, κ, β, γ, n_steps, X_init, snap_idx += 1 # === Draw D_{t+1} and observe outcome === - d = np.random.geometric(p) - 1 + d = rng.geometric(p) - 1 reward = min(x, d) - c * a - κ * (a > 0) x_next = max(x - d, 0) + a @@ -630,8 +629,8 @@ def q_learning_rs_kernel(K, p, c, κ, β, γ, n_steps, X_init, # === Behavior policy: ε-greedy (uses a_next, the argmin action) === x = x_next - if np.random.random() < ε: - a = np.random.randint(0, K - x + 1) + if rng.random() < ε: + a = rng.integers(0, K - x + 1) else: a = a_next ε = max(ε_min, ε * ε_decay) @@ -649,8 +648,9 @@ def q_learning_rs(model, n_steps=20_000_000, X_init=0, K = len(x_values) - 1 if snapshot_steps is None: snapshot_steps = np.array([], dtype=np.int64) + rng = np.random.default_rng(seed) return q_learning_rs_kernel(K, p, c, κ, β, γ, n_steps, X_init, - ε_init, ε_min, ε_decay, q_init, snapshot_steps, seed) + ε_init, ε_min, ε_decay, q_init, snapshot_steps, rng) ``` ### Running Q-learning @@ -721,7 +721,8 @@ X_init = K // 2 sim_seed = 5678 # Optimal policy -X_opt = sim_inventories(ts_length, σ_star, model.p, X_init, seed=sim_seed) +X_opt = sim_inventories(ts_length, σ_star, model.p, + np.random.default_rng(sim_seed), X_init) axes[0].plot(X_opt, alpha=0.7) axes[0].set_ylabel("inventory") axes[0].set_title("Optimal (VFI)") @@ -730,7 +731,8 @@ axes[0].set_ylim(0, K + 2) # Q-learning snapshots for i in range(n_snaps): σ_snap = snapshots[i] - X = sim_inventories(ts_length, σ_snap, model.p, X_init, seed=sim_seed) + X = sim_inventories(ts_length, σ_snap, model.p, + np.random.default_rng(sim_seed), X_init) axes[i + 1].plot(X, alpha=0.7) axes[i + 1].set_ylabel("inventory") axes[i + 1].set_title(f"Step {snap_steps[i]:,}") diff --git a/sync/ledger.yml b/sync/ledger.yml index 1a5e1d1..008e779 100644 --- a/sync/ledger.yml +++ b/sync/ledger.yml @@ -25,8 +25,8 @@ lectures/amss.md: path: amss.md digest: 85751eb0279687330e00e82700733bb8f53321a0915ef4c13b60f801a1dc4c67 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/opt_tax_recur/crra_utility.py - _static/_shared/opt_tax_recur/log_utility.py @@ -51,8 +51,8 @@ lectures/amss2.md: path: amss2.md digest: a6e68794f0ed3199d4eedf122774bd9c106048440e4c4257c8c641d00605dba7 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/amss2/crra_utility.py - _static/_shared/amss2/recursive_allocation.py @@ -77,8 +77,8 @@ lectures/amss3.md: path: amss3.md digest: ceea4d35a8f058fa19ad6e52f9c2413af081e93bb25fd6af868600d784fb32c2 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/amss2/crra_utility.py - _static/_shared/amss2/recursive_allocation.py @@ -112,8 +112,8 @@ lectures/calvo.md: path: calvo.md digest: 2f6d460dbde4a441263f6f7b3cae0dadb47db82c6f233bff9fef138958c921ef promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/calvo_abreu.md: @@ -126,8 +126,8 @@ lectures/calvo_abreu.md: path: calvo_abreu.md digest: 1823f9e91812b518980ddb18306b180d9b69bff2ab675acb712bed99337f1a50 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/calvo_machine_learn.md: @@ -140,27 +140,32 @@ lectures/calvo_machine_learn.md: path: calvo_machine_learn.md digest: 4a53ab7e798de136e7f51661d1bc2488f169a65b9c4d2f668daebbfd1828db36 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/career.md: canonical: intermediate + interim: true sources: - series: intermediate path: career.md - digest: f76621615f2331f0cfb38ee10d80f667f805b9fd5b4e7b73a75cd216889f16c2 + digest: ada259685f814573d4cb0f1550c411e37f1f3c0d2e8ba9fa2362718adc7e8535 + divergent: - series: dp-test path: career.md digest: f76621615f2331f0cfb38ee10d80f667f805b9fd5b4e7b73a75cd216889f16c2 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: + - _static/_shared/_admonition/gpu.md - _static/career/career_solutions_ex1_py.png rewrites: - from: /_static/lecture_specific/career/career_solutions_ex1_py.png to: _static/career/career_solutions_ex1_py.png + - from: _admonition/gpu.md + to: _static/_shared/_admonition/gpu.md lectures/chang_credible.md: canonical: advanced sources: @@ -171,8 +176,8 @@ lectures/chang_credible.md: path: chang_credible.md digest: 36268e1348f815b004fae2b3cec0ff8e3f61b8180406b5ec7696eaa9bed02535 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/chang_credible/changecon.py rewrites: @@ -188,8 +193,8 @@ lectures/chang_ramsey.md: path: chang_ramsey.md digest: c92ef6f9ebc6c9bb0d5c04a45be8597c4b1b3f7bcf126eaddf7852825d8ca7a8 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/chang_credible/changecon.py rewrites: @@ -207,8 +212,8 @@ lectures/cons_news.md: path: cons_news.md digest: 8fd53563530440e3bfc83f1f9797e2fa0091fe34614c519bbad7b1c5b8bb3740 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/cross_product_trick.md: @@ -221,8 +226,8 @@ lectures/cross_product_trick.md: path: cross_product_trick.md digest: ff84e07d006be53be60cbdda4902e655c8503db44be80b9fc728f1147054663d promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/discrete_dp.md: @@ -235,8 +240,8 @@ lectures/discrete_dp.md: path: discrete_dp.md digest: 27e3a18c1273df29bb75473c7d8db725c0a2d903d26dcc80a013668c77cba389 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/discrete_dp/finite_dp_simple_og.png - _static/discrete_dp/finite_dp_simple_og2.png @@ -255,22 +260,24 @@ lectures/dyn_stack.md: path: dyn_stack.md digest: 0f7d536a88df8d6aa089f495136ae17721a64cc7c0228252f1dc7362a2d85f25 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/ifp_advanced.md: canonical: intermediate + interim: true sources: - series: intermediate path: ifp_advanced.md - digest: 609edbee0503f199f0bfddd5cae27dfe602bedcce8a57ac1e397dc42009f5a4d + digest: 46107f290587bf6ce7d2a6a8c60db424c83fb45f0978295d04b1a8cd75993da1 + divergent: - series: dp-test path: ifp_advanced.md digest: 609edbee0503f199f0bfddd5cae27dfe602bedcce8a57ac1e397dc42009f5a4d promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -286,8 +293,8 @@ lectures/ifp_discrete.md: path: ifp_discrete.md digest: f682b80bc0b0d2d57e6b803ed078c5b8f118e780b4e897fa5aac30b8448c9da6 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -295,16 +302,18 @@ lectures/ifp_discrete.md: to: _static/_shared/_admonition/gpu.md lectures/ifp_egm.md: canonical: intermediate + interim: true sources: - series: intermediate path: ifp_egm.md - digest: 55caf3340fb40ef5299d7fc440985172ec94e9c495552ab50c74b38a8dbf9f11 + digest: 743c023b0801d0f4b1cabd6f63368035c7fce11a1f038a30c7db550d5e1f36b0 + divergent: - series: dp-test path: ifp_egm.md digest: 55caf3340fb40ef5299d7fc440985172ec94e9c495552ab50c74b38a8dbf9f11 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -312,16 +321,18 @@ lectures/ifp_egm.md: to: _static/_shared/_admonition/gpu.md lectures/ifp_egm_transient_shocks.md: canonical: intermediate + interim: true sources: - series: intermediate path: ifp_egm_transient_shocks.md - digest: a8df3aa52e6e2a9eeb123da30c6af849def7a425a43f38d73e7bd7fe50a118d8 + digest: 901acde648b4b471391d1fd73a4f0ae080b29974eed04cde59c6ac7313cf64da + divergent: - series: dp-test path: ifp_egm_transient_shocks.md digest: a8df3aa52e6e2a9eeb123da30c6af849def7a425a43f38d73e7bd7fe50a118d8 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -337,8 +348,8 @@ lectures/ifp_opi.md: path: ifp_opi.md digest: 6d065237901c8cc4b37e13396d66467c5f76a576a17e20b3a4e281d133e18925 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -346,32 +357,39 @@ lectures/ifp_opi.md: to: _static/_shared/_admonition/gpu.md lectures/inventory_q.md: canonical: intermediate + interim: true sources: - series: intermediate path: inventory_q.md - digest: a4a1789b0aebba6692f25c9483f68b1de809f6734b5a416f5a037c98ee75e4fb + digest: a6d622fbd14671daf3101a25b1d202ecd9e23ad572a77daeba4e9edbbddce022 + divergent: - series: dp-test path: inventory_q.md digest: a4a1789b0aebba6692f25c9483f68b1de809f6734b5a416f5a037c98ee75e4fb promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/jv.md: canonical: intermediate + interim: true sources: - series: intermediate path: jv.md - digest: 251f24c59118ea6220df9e31ae07119f92f6fda2df3ce85c573e7e1361a8b858 + digest: 1a5c73c392d5fc108913ef756797cc2975908c87617aa404707ca417b42a0ec2 + divergent: - series: dp-test path: jv.md digest: 251f24c59118ea6220df9e31ae07119f92f6fda2df3ce85c573e7e1361a8b858 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 - assets: [] - rewrites: [] + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c + assets: + - _static/_shared/_admonition/gpu.md + rewrites: + - from: _admonition/gpu.md + to: _static/_shared/_admonition/gpu.md lectures/lagrangian_lqdp.md: canonical: dp-test interim: true @@ -384,8 +402,8 @@ lectures/lagrangian_lqdp.md: path: lagrangian_lqdp.md digest: a0435e3185b1fa81ff2f0e1030f84344c49ed705a2f1f47f9f6d97a72d42fe80 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/lq_inventories.md: @@ -398,8 +416,8 @@ lectures/lq_inventories.md: path: lq_inventories.md digest: 63cd01cf845cc3b5b4c28e863f587cf16d8a7f7b45e18950c9e8c90e5779bded promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/lqcontrol.md: @@ -412,10 +430,10 @@ lectures/lqcontrol.md: divergent: - series: intermediate path: lqcontrol.md - digest: e8d900092a185298fa4a29ff2f32c023e8814329abc931c4f7f5bd2075a64f8b + digest: 4fcebb9b55ef08af05cb053dbad47e0b9e05b6fc7f8a559ead663fc61ef3c465 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/lqcontrol/solution_lqc_ex1.png - _static/lqcontrol/solution_lqc_ex2.png @@ -443,8 +461,8 @@ lectures/lqramsey.md: path: lqramsey.md digest: 71f52e0b79943489332d2ee86111dd3d1edc59ceb7015a3ba86dfbc0fda991cc promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/lqramsey/firenze.pdf rewrites: @@ -460,22 +478,24 @@ lectures/markov_jump_lq.md: path: markov_jump_lq.md digest: 1b93d3887409f0ca8f6d4e24a2cd843918755e6d78f653e3f06f62e3e07e9c7f promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/mccall_fitted_vfi.md: canonical: intermediate + interim: true sources: - series: intermediate path: mccall_fitted_vfi.md - digest: e3de90c73a8770896c1a35fe0bfc8f2e98f2e05828d26180617715277f5bbd08 + digest: f233d0a11b20263e0caf532021e565bbd8cecdefd303368e34bc0593d733d68d + divergent: - series: dp-test path: mccall_fitted_vfi.md digest: e3de90c73a8770896c1a35fe0bfc8f2e98f2e05828d26180617715277f5bbd08 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -483,16 +503,18 @@ lectures/mccall_fitted_vfi.md: to: _static/_shared/_admonition/gpu.md lectures/mccall_model.md: canonical: intermediate + interim: true sources: - series: intermediate path: mccall_model.md - digest: 513e33f31e047312131de0983a3e06a500b5768b2f5900fcbad799651c5f9fb7 + digest: a2752f6e58d15a399e2e611531e2b075d4e889947f904a41520c15e26f2a0160 + divergent: - series: dp-test path: mccall_model.md digest: 513e33f31e047312131de0983a3e06a500b5768b2f5900fcbad799651c5f9fb7 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -508,10 +530,10 @@ lectures/mccall_model_with_sep_markov.md: divergent: - series: intermediate path: mccall_model_with_sep_markov.md - digest: c3c28f85e7b878b3c89dad3948f4b3db1b5feb889baad07c785f25980b896691 + digest: ccb7530f8b60fb371f835ac1073e82a9500686367983e90e6d7b045359e413b8 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -527,8 +549,8 @@ lectures/mccall_model_with_separation.md: path: mccall_model_with_separation.md digest: a7e57bf7d78a21e31acfb138b2536e244a9bfebd5bc512fea0f87ca45ac1094a promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -536,16 +558,18 @@ lectures/mccall_model_with_separation.md: to: _static/_shared/_admonition/gpu.md lectures/mccall_persist_trans.md: canonical: intermediate + interim: true sources: - series: intermediate path: mccall_persist_trans.md - digest: 01aaa2b9476da6e045a19ebd785245acb3f77bb8c26c9163c16d21a612164ba3 + digest: 99da12637572735806c45842fa1190a0eedbdea78211a1abc542effe61000efb + divergent: - series: dp-test path: mccall_persist_trans.md digest: 01aaa2b9476da6e045a19ebd785245acb3f77bb8c26c9163c16d21a612164ba3 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -553,16 +577,18 @@ lectures/mccall_persist_trans.md: to: _static/_shared/_admonition/gpu.md lectures/mccall_q.md: canonical: intermediate + interim: true sources: - series: intermediate path: mccall_q.md - digest: 1ba2a8c13e77dd343d3d63b77d9cde1f0277f76ba6305d9d08d03d4746338b47 + digest: 96dc46004295622ebb07bc784943109c00d9768cdc58f564a24b359f870429e5 + divergent: - series: dp-test path: mccall_q.md digest: 1ba2a8c13e77dd343d3d63b77d9cde1f0277f76ba6305d9d08d03d4746338b47 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/odu.md: @@ -575,24 +601,26 @@ lectures/odu.md: divergent: - series: intermediate path: odu.md - digest: b7f0f9df5d1dcc235a55ee6f89cc4e1641fce7015c6736a3ea1ba4b417b016f1 + digest: 95015586259d5a002aafcec3ef00c288bff8968cdf6f1b46af840326fd21d0bf promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/opt_tax_recur.md: canonical: advanced + interim: true sources: - series: advanced path: opt_tax_recur.md - digest: a0402da1277b47c250f842bd3b7fd7e633098f9b3adf59f1281487e7d74852bd + digest: 6d2c023987ff96f1edf5d595b3b780793c15d351a7357743c4b9ce49d5e08440 + divergent: - series: dp-test path: opt_tax_recur.md digest: a0402da1277b47c250f842bd3b7fd7e633098f9b3adf59f1281487e7d74852bd promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/opt_tax_recur/crra_utility.py - _static/_shared/opt_tax_recur/log_utility.py @@ -619,8 +647,8 @@ lectures/os.md: path: os.md digest: 4b48ab4e73770d12c29a9dc32c05d84a114e803263c50a1b462e566e69a4757d promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/os_egm.md: @@ -633,10 +661,10 @@ lectures/os_egm.md: divergent: - series: intermediate path: os_egm.md - digest: 614396185408062057f11f9f0251a2772b598c3cb20cf2eacd743a59db9df327 + digest: 98e8079178e9c5499b9b455c73e43fecdf60ef97c1c8ae47385a0cbede8e84d3 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/os_egm_jax.md: @@ -649,10 +677,10 @@ lectures/os_egm_jax.md: divergent: - series: intermediate path: os_egm_jax.md - digest: d5c04b6889428eceaa55ff27f3cace87626d8d022f657cd911db4676f8fea871 + digest: 44a03a37cfbed936a2088330c4275680da0370ad14a4380c89a75b744eb3c4ff promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/_shared/_admonition/gpu.md rewrites: @@ -668,36 +696,40 @@ lectures/os_numerical.md: path: os_numerical.md digest: 7037f0003d74dda30773a4425ebd724871aa052c3f4ffa8294256c49f53b6283 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/os_stochastic.md: canonical: intermediate + interim: true sources: - series: intermediate path: os_stochastic.md - digest: c4eec8485da3931391d0c7b2f42c904da4e81e50deb80045f85eb317810d3319 + digest: 3ed430247f0bd1154b258555fb9f0981031886d69d9708ed41b80669b31faa3f + divergent: - series: dp-test path: os_stochastic.md digest: c4eec8485da3931391d0c7b2f42c904da4e81e50deb80045f85eb317810d3319 promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/os_time_iter.md: canonical: intermediate + interim: true sources: - series: intermediate path: os_time_iter.md - digest: d27b7af1e2dd2a8c2d92f37bf2af09d7d1bd565d3b2583c02c0b11cc131058ec + digest: 44ebdf94f5d4260da014296bc45c0f85789955ff392a2dc6feddc8c0c8df4161 + divergent: - series: dp-test path: os_time_iter.md digest: d27b7af1e2dd2a8c2d92f37bf2af09d7d1bd565d3b2583c02c0b11cc131058ec promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/perm_income.md: @@ -710,10 +742,10 @@ lectures/perm_income.md: divergent: - series: intermediate path: perm_income.md - digest: d3b45ff0e443acfef5305702a56c59c175d7f23aedcdab835957f1ddef181132 + digest: f3d103c352803d552997d9e2376db8bc9fc116ac362efc544c3be609f690642d promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/perm_income_cons.md: @@ -728,22 +760,24 @@ lectures/perm_income_cons.md: path: perm_income_cons.md digest: d2024dfba39a5dec505bb7fe07fd48111cda47c004a330a187cf881162bbb8aa promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/rs_inventory_q.md: canonical: intermediate + interim: true sources: - series: intermediate path: rs_inventory_q.md - digest: 697dd978247aa755bd450db25b2ee1d8e905087b9a51811e071935fc6b9d71ed + digest: adc8397196a13e59c35cab1d8c7af786a40cec5aaddec162e63f5ed5784890f4 + divergent: - series: dp-test path: rs_inventory_q.md digest: 697dd978247aa755bd450db25b2ee1d8e905087b9a51811e071935fc6b9d71ed promoted_at: - intermediate: 8cfba4c90ebc08d3e51718ee65246ac249305ce0 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intermediate: 3b5fde644bbb15d51b398d0232cd6429d99a0a03 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/short_path.md: @@ -756,8 +790,8 @@ lectures/short_path.md: path: short_path.md digest: 1f128140aa71e0d62a9a9556ebe5db39bde1583b2bed5e6a1879f4018cc31671 promoted_at: - intro: 29000a444f1ab49ea2a268ab320a96d4f2a30c12 - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + intro: c8b8a71c42820c5730a3c0266f99ffb8e1323472 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: - _static/short_path/graph.png - _static/short_path/graph2.png @@ -782,8 +816,8 @@ lectures/smoothing.md: path: smoothing.md digest: 7e4c36ee1b3841cac25a953f58a4e89511d43dc334a68097665e0005da6b2730 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/smoothing_tax.md: @@ -796,8 +830,8 @@ lectures/smoothing_tax.md: path: smoothing_tax.md digest: c0042439a0819c94c956bf20edfbc017a44701be0aefafc63f8fd8ddbfcb343d promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/tax_smoothing_1.md: @@ -810,8 +844,8 @@ lectures/tax_smoothing_1.md: path: tax_smoothing_1.md digest: fef159f536674045854bf966a19549f9ce19c2b07306ab82f2ab85540b425dca promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/tax_smoothing_2.md: @@ -824,8 +858,8 @@ lectures/tax_smoothing_2.md: path: tax_smoothing_2.md digest: bfc852631857de8614d587d8c15c6b4b62326760896183cb389f5b69ed7b2a12 promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/tax_smoothing_3.md: @@ -838,8 +872,8 @@ lectures/tax_smoothing_3.md: path: tax_smoothing_3.md digest: 09400c45f829d29738ff0863f5397c71389a8a07e3a3cb3a6e3a4a679248576c promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] lectures/un_insure.md: @@ -852,8 +886,8 @@ lectures/un_insure.md: path: un_insure.md digest: f2fe2ea8750a374ea597637194b9e001ffa6dfdb86860e9e1aa8ef3e7e33a80f promoted_at: - advanced: 71f2bf77c24109cc6513b868e71732a94818e8bc - dp-test: 1fe6c64a850daf59823ae3ac9cf69d41fa360287 + advanced: 7f42c5e2b7e4d7f416da73b9789fdc21c21d2917 + dp-test: 8d26b9349f55f6f185395c82279518bd3ba6a51c assets: [] rewrites: [] assets: