diff --git a/lectures/_static/quant-econ.bib b/lectures/_static/quant-econ.bib
index e83d20a9..2cb54c37 100644
--- a/lectures/_static/quant-econ.bib
+++ b/lectures/_static/quant-econ.bib
@@ -3269,3 +3269,69 @@ @article{CoibionGorodnichenko2015
pages = {2644--2678},
doi = {10.1257/aer.20130921}
}
+
+@article{Uhlig2005,
+ author = {Uhlig, Harald},
+ title = {What Are the Effects of Monetary Policy on Output? Results from an Agnostic Identification Procedure},
+ journal = {Journal of Monetary Economics},
+ year = {2005},
+ volume = {52},
+ number = {2},
+ pages = {381--419},
+ doi = {10.1016/j.jmoneco.2004.05.007}
+}
+
+@incollection{Uhlig2017,
+ author = {Uhlig, Harald},
+ title = {Shocks, Sign Restrictions, and Identification},
+ booktitle = {Advances in Economics and Econometrics: Eleventh World Congress},
+ editor = {Honoré, Bo and Pakes, Ariel and Piazzesi, Monika and Samuelson, Larry},
+ volume = {2},
+ chapter = {4},
+ pages = {95--127},
+ publisher = {Cambridge University Press},
+ year = {2017},
+ doi = {10.1017/9781108227223.004}
+}
+
+@article{DelNegroSchorfheide2004,
+ author = {Del Negro, Marco and Schorfheide, Frank},
+ title = {Priors from General Equilibrium Models for {VARs}},
+ journal = {International Economic Review},
+ year = {2004},
+ volume = {45},
+ number = {2},
+ pages = {643--673},
+ doi = {10.1111/j.1468-2354.2004.00139.x}
+}
+
+@article{BaumeisterHamilton2015,
+ author = {Baumeister, Christiane and Hamilton, James D.},
+ title = {Sign Restrictions, Structural Vector Autoregressions, and Useful Prior Information},
+ journal = {Econometrica},
+ year = {2015},
+ volume = {83},
+ number = {5},
+ pages = {1963--1999},
+ doi = {10.3982/ECTA12356}
+}
+
+@article{LeeperTraumWalker2017,
+ author = {Leeper, Eric M. and Traum, Nora and Walker, Todd B.},
+ title = {Clearing Up the Fiscal Multiplier Morass},
+ journal = {American Economic Review},
+ year = {2017},
+ volume = {107},
+ number = {8},
+ pages = {2409--2454},
+ doi = {10.1257/aer.20111196}
+}
+
+@book{Geweke2010,
+ author = {Geweke, John},
+ title = {Complete and Incomplete Econometric Models},
+ publisher = {Princeton University Press},
+ address = {Princeton, NJ},
+ year = {2010},
+ doi = {10.1515/9781400835249}
+}
diff --git a/lectures/_toc.yml b/lectures/_toc.yml
index 91de32ca..7dafb53e 100644
--- a/lectures/_toc.yml
+++ b/lectures/_toc.yml
@@ -40,6 +40,7 @@ parts:
- file: cattle_cycles
- file: hs_invertibility_example
- file: supply_demand_var
+ - file: induced_priors
- caption: Risk, Model Uncertainty, and Robustness
numbered: true
chapters:
diff --git a/lectures/induced_priors.md b/lectures/induced_priors.md
new file mode 100644
index 00000000..80609a73
--- /dev/null
+++ b/lectures/induced_priors.md
@@ -0,0 +1,1721 @@
+---
+jupytext:
+ text_representation:
+ extension: .md
+ format_name: myst
+kernelspec:
+ display_name: Python 3
+ language: python
+ name: python3
+---
+
+(induced_priors)=
+```{raw} html
+
+```
+
+```{index} single: python
+```
+
+# Induced Priors for Impulse Responses
+
+## Overview
+
+This lecture uses the market for a single good studied in {doc}`supply_demand_var` as a laboratory.
+
+We imagine a **structural modeler** who believes that model and knows all of its parameters except two
+adjustment-cost parameters, the supplier's $g_s$ and the demander's $g_d$.
+
+The modeler expresses uncertainty about $(g_s, g_d)$ with a Bayesian prior.
+
+Because every object that the model implies --- impulse responses, autocovariances, the vector
+autoregression that an econometrician would fit to prices and quantities --- is a function of
+$(g_s, g_d)$, the prior on $(g_s, g_d)$ **induces** a prior over each of those objects.
+
+We approximate these induced priors by Monte Carlo.
+
+In this lecture, we shall
+
+* compute the induced prior over the impulse responses of price $p_t$ and quantity $q_t$ to the supply
+ shock $w_{st}$ and the demand shock $w_{dt}$ that hit the agents' information sets,
+* compute the induced prior over the autocovariance function of $(q_t, p_t)$ --- everything that an
+ infinitely long sample could reveal,
+* compute the induced prior over the population **vector autoregression** for $(q_t, p_t)$: the
+ covariance matrix of its innovations and its impulse responses under two standard recursive
+ (Cholesky) orthogonalizations, one that puts $q_t$ first and one that puts $p_t$ first,
+* compare the induced priors over the VAR impulse responses with those over the structural impulse
+ responses,
+* use the induced prior to construct **sign restrictions motivated by economic theory** in the spirit
+ of {cite:t}`Uhlig2005,Uhlig2017`, and then ask what those sign restrictions can and cannot
+ recover from a VAR, and
+* widen the prior, and then change its shape from uniform to log-uniform, to see how the induced
+ priors, the region in which the supply shock is fundamental, and the performance of sign restrictions
+ change.
+
+The last step brings us back to the shock-invertibility issue that occupied
+{doc}`supply_demand_var` and {doc}`hs_invertibility_example`.
+
+We shall see that sign restrictions that are *true* --- they hold with prior probability one under the
+modeler's theory --- can nonetheless identify a set of VAR impulse responses that *excludes* the true
+response to a supply shock.
+
+In addition to what's in Anaconda, this lecture uses the quantecon library.
+
+```{code-cell} ipython
+---
+tags: [hide-output]
+---
+!pip install --upgrade quantecon
+```
+
+We'll make these imports:
+
+```{code-cell} python3
+import numpy as np
+import quantecon as qe
+import matplotlib.pyplot as plt
+from scipy.linalg import solve_discrete_lyapunov
+```
+
+## Background: sign restrictions
+
+{cite:t}`Uhlig2005` proposed an "agnostic" way to identify shocks in a vector autoregression.
+
+Let $u_t$ be the one-step-ahead prediction error of a VAR, with covariance matrix $V = \mathbb{E}[u_t u_t^\top]$.
+
+A researcher wants to write $u_t = A \epsilon_t$, where $\epsilon_t$ is a vector of mutually
+uncorrelated, unit-variance **structural** shocks, so that $V = A A^\top$.
+
+The covariance matrix $V$ does not pin down $A$.
+
+If $L$ is the lower-triangular Cholesky factor of $V$, every factorization takes the form
+
+$$
+A = L R , \qquad R R^\top = I ,
+$$
+
+so that a column $a$ of $A$ --- what Uhlig calls an **impulse vector** --- satisfies $a = L r$ for some
+vector $r$ of unit length.
+
+A Cholesky decomposition picks $R = I$, and with it a recursive "causal ordering" of the variables.
+
+Uhlig instead imposes inequality restrictions on the *signs* of impulse responses, for example that a
+contractionary monetary policy shock does not raise prices for $K$ periods, and treats every rotation
+$R$ that satisfies the restrictions as admissible.
+
+Drawing $r$ uniformly from the unit sphere (the **Haar** measure) and discarding draws that violate the
+restrictions gives a sample from a prior that is flat on the admissible set.
+
+{cite:t}`Uhlig2017` illustrates the logic with a static supply-and-demand example in which
+"supply slopes up, demand slopes down" delivers the sign pattern
+
+$$
+\begin{array}{c|cc}
+ & \epsilon_S & \epsilon_D \\ \hline
+P & - & + \\
+Q & + & +
+\end{array}
+$$
+
+and he summarizes his advice with two principles: *if you know it, impose it*; *if you do not know
+it, do not impose it*.
+
+Where do sign restrictions come from?
+
+They are supposed to be implications of economic theory that are robust across the range of theories,
+or of parameter values, that an audience finds plausible.
+
+An induced prior makes that idea precise: a sign that holds for *every* parameter vector in the
+support of the prior is an implication of the theory that a believer can impose with confidence.
+
+## The structural model and the prior
+
+The model is the one in {doc}`supply_demand_var`, which we briefly recall.
+
+A representative supplier and a representative demander each solve a linear-quadratic problem,
+taking the price process as given.
+
+The supplier faces adjustment costs $\tfrac{g_s}{2}(q_t - q_{t-1})^2$ and a cost shock
+$s_t = B_s(L) w_{st}$; the demander values durable services $a(L) q_t$ with adjustment cost
+$\tfrac{g_d}{2}(a(L)q_t)^2$ and faces a demand shock $d_t = B_d(L) w_{dt}$.
+
+The shocks $w_{st}, w_{dt}$ are mutually uncorrelated white noises with variances $\sigma_s^2 = 0.5$ and
+$\sigma_d^2 = 4$.
+
+The rational expectations equilibrium has the state-space representation
+
+$$
+X_{t+1} = A_F X_t + C \epsilon_{t+1}, \qquad z_t = \begin{bmatrix} q_t \\ p_t \end{bmatrix} = G_z X_t ,
+$$
+
+with $\epsilon_t = (w_{st}, w_{dt})$.
+
+The modeler knows
+
+$$
+h_s = h_d = 1, \quad \beta = 1/1.05, \quad a(L) = 1 + .8L + .6L^2 + .4L^3 + .2L^4,
+$$
+
+$$
+B_d(L) = (1+.6L)(1+.4L)(1+.2L), \qquad B_s(L) = (1-.8L)(1+.4L)(1+.2L),
+$$
+
+and the two shock variances.
+
+The modeler does *not* know the adjustment-cost parameters.
+
+Instead of the values $g_s = 10$ and $g_d = 0.1$ used in {doc}`supply_demand_var`, the modeler has
+independent uniform priors
+
+$$
+g_s \sim U[8, 12], \qquad g_d \sim U[0.05, 0.15] .
+$$
+
+We continue to call $(g_s, g_d) = (10, 0.1)$ the **true** parameter values.
+
+```{code-cell} python3
+h_s = h_d = 1.0
+β = 1 / 1.05
+a = np.array([1, .8, .6, .4, .2]) # a(L): coefficients a0..a4
+
+# MA polynomials for the shocks (coefficients on L^0..L^3)
+B_d = np.convolve(np.convolve([1, .6], [1, .4]), [1, .2])
+B_s = np.convolve(np.convolve([1, -.8], [1, .4]), [1, .2])
+
+σ_s2, σ_d2 = 0.5, 4.0 # shock variances
+scale = np.diag([np.sqrt(σ_s2), np.sqrt(σ_d2)])
+
+g_s_true, g_d_true = 10.0, 0.1 # the "true" values
+g_s_lo, g_s_hi = 8.0, 12.0 # prior support for g_s
+g_d_lo, g_d_hi = 0.05, 0.15 # prior support for g_d
+```
+
+We solve for the equilibrium exactly as in {doc}`supply_demand_var`, as the optimal linear regulator of
+a fictitious planner.
+
+```{code-cell} python3
+def companion_shift(n):
+ "Companion matrix that shifts an n-vector of lagged shocks."
+ A = np.zeros((n, n))
+ A[1:, :-1] = np.eye(n - 1)
+ return A
+
+def solve_equilibrium(h_s, h_d, g_s, g_d, β, a, B_s, B_d):
+ """
+ Solve the market equilibrium as a planner's optimal linear regulator.
+
+ State x = [q_{t-1}, ..., q_{t-4}, w_s(t..t-3), w_d(t..t-3)] (dim 12)
+ Control u = q_t.
+
+ Returns the closed-loop transition A_F, shock loading C, the selector
+ G_z mapping the state into (q_t, p_t), and the selectors for s_t, d_t.
+ """
+ n_q = 4
+ # exogenous companion for the two MA(3) shock processes
+ A_w = np.zeros((8, 8))
+ A_w[:4, :4] = companion_shift(4)
+ A_w[4:, 4:] = companion_shift(4)
+ C_w = np.zeros((8, 2))
+ C_w[0, 0], C_w[4, 1] = 1.0, 1.0 # inject (w_s, w_d)
+
+ n = n_q + 8
+ A = np.zeros((n, n))
+ A[1:n_q, 0:n_q - 1] = np.eye(n_q - 1) # q lags shift
+ A[n_q:, n_q:] = A_w
+ B = np.zeros((n, 1))
+ B[0, 0] = 1.0 # new q_{t-1} = q_t
+ C = np.zeros((n, 2))
+ C[n_q:, :] = C_w
+
+ def q_lag(k): # selector for q_{t-k}
+ e = np.zeros(n)
+ e[k - 1] = 1.0
+ return e
+
+ s_sel = np.zeros(n)
+ s_sel[n_q:n_q + 4] = B_s # s_t = s_sel @ x
+ d_sel = np.zeros(n)
+ d_sel[n_q + 4:] = B_d
+ q1 = q_lag(1)
+ a_vec = sum(a[k] * q_lag(k) for k in range(1, 5)) # lagged part of a(L)q_t
+
+ # one-period loss = -(surplus), written as u'Qu + x'Rx + 2 u'Nx
+ Q = np.array([[(h_s + h_d) / 2 + g_s / 2 + g_d / 2 * a[0]**2]])
+ R = g_d / 2 * np.outer(a_vec, a_vec) + g_s / 2 * np.outer(q1, q1)
+ R += 1e-5 * np.eye(n) # tiny ridge for the Riccati solver
+ N = (g_d * a[0] * a_vec / 2 - g_s * q1 / 2
+ + (s_sel + d_sel) / 2).reshape(1, n)
+
+ lq = qe.LQ(Q, R, A, B, C, N=N, beta=β)
+ P, F, _ = lq.stationary_values()
+ F = np.asarray(F).reshape(1, n)
+ A_F = A - B @ F
+
+ # price p_t = s_t + h_s q_t + g_s[(1+β) q_t - q_{t-1} - β E_t q_{t+1}]
+ F_q = -F # q_t = F_q @ x
+ E_q1 = -F @ A_F # E_t q_{t+1} = E_q1 @ x
+ G_p = (s_sel + h_s * F_q[0] + g_s * (1 + β) * F_q[0]
+ - g_s * q1 - g_s * β * E_q1[0]).reshape(1, n)
+ G_z = np.vstack([F_q, G_p]) # (q_t, p_t) = G_z @ x
+ return A_F, C, G_z, F, s_sel, d_sel
+```
+
+### A sign convention
+
+In {doc}`supply_demand_var`, a positive $w_{st}$ *raises* the supplier's costs and a positive $w_{dt}$
+*lowers* the demander's willingness to pay, so both push quantity down.
+
+To match the convention in {cite}`Uhlig2017` that a positive shock raises quantity, we work with
+
+$$
+\tilde w_{st} = -w_{st} \quad \text{(a favorable supply shock)}, \qquad
+\tilde w_{dt} = -w_{dt} \quad \text{(a positive demand shock)},
+$$
+
+each scaled to one standard deviation.
+
+From now on "the supply shock" and "the demand shock" refer to $\tilde w_{st}$ and $\tilde w_{dt}$.
+
+## Induced priors by Monte Carlo
+
+Let $\theta = (g_s, g_d)$ have prior density $\pi(\theta)$ and let $f(\theta)$ be any object that the
+model implies, for example the impulse response of $p_{t+j}$ to $\tilde w_{dt}$.
+
+The **induced prior** over $f$ is the probability distribution of the random variable $f(\theta)$
+when $\theta \sim \pi$.
+
+It is the push-forward of $\pi$ through the mapping $f$.
+
+Typically $f$ is a complicated nonlinear function --- here it requires solving a Riccati equation, and
+for the VAR objects a second Riccati equation for a Kalman filter --- so we can't write down the induced
+prior in closed form.
+
+Monte Carlo is the natural tool:
+
+1. draw $\theta_i \sim \pi$ for $i = 1, \ldots, N$,
+2. compute $f(\theta_i)$,
+3. treat $\{f(\theta_i)\}_{i=1}^N$ as a sample from the induced prior.
+
+{cite:t}`Geweke2010` calls this **prior predictive analysis**.
+
+{cite:t}`LeeperTraumWalker2017` use it to compute the range of
+government-spending multipliers --- ratios of discounted impulse responses --- that alternative DSGE
+specifications can produce before they confront data.
+
+For impulse response functions we summarize the sample by pointwise quantiles, horizon by horizon.
+
+As {cite:t}`Uhlig2017` reminds us (his Principle 17), a band built this way describes a
+collection of *marginal* distributions, one for each horizon; the curve that joins the pointwise
+medians is generally not the impulse response for any single draw $\theta_i$.
+
+For each draw we compute four objects.
+
+* **Structural impulse responses** $D_j = G_z A_F^{\,j} \tilde C$, where $\tilde C = -C\,
+ \mathrm{diag}(\sigma_s, \sigma_d)$ loads the sign-flipped, unit-standard-deviation shocks.
+* **Autocovariances** $\Gamma_j = \mathbb{E}[z_{t+j} z_t^\top] = G_z A_F^{\,j} \Sigma_X G_z^\top$, where
+ $\Sigma_X = A_F \Sigma_X A_F^\top + \tilde C \tilde C^\top$ is the stationary covariance of the state.
+* **The innovation covariance** $V = \mathbb{E}[a_t a_t^\top]$ of the population VAR for $z_t$.
+* **The Wold moving-average coefficients** $C_j$ of the population VAR, with $C_0 = I$.
+
+As in {doc}`supply_demand_var`, the last two come from the stationary Kalman filter, whose innovations
+representation is the Wold representation $z_t = \sum_{j \ge 0} C_j a_{t-j}$.
+
+```{code-cell} python3
+T = 20 # number of horizons j = 0, ..., T-1
+
+def model_objects(g_s, g_d, T=T):
+ "Structural IRFs, autocovariances, and population VAR objects at (g_s, g_d)."
+ A_F, C, G_z, *_ = solve_equilibrium(h_s, h_d, g_s, g_d, β, a, B_s, B_d)
+ C_tilde = -C @ scale # favorable supply shock, positive demand shock
+
+ # structural impulse responses D_j = G_z A_F^j C_tilde
+ D = np.empty((T, 2, 2))
+ M = C_tilde.copy()
+ for j in range(T):
+ D[j] = G_z @ M
+ M = A_F @ M
+
+ # autocovariances Γ_j = E z_{t+j} z_t'
+ Σ_X = solve_discrete_lyapunov(A_F, C_tilde @ C_tilde.T)
+ Γ = np.empty((T, 2, 2))
+ M = Σ_X.copy()
+ for j in range(T):
+ Γ[j] = G_z @ M @ G_z.T
+ M = A_F @ M
+
+ # Wold (innovations) representation from the stationary Kalman filter
+ lss = qe.LinearStateSpace(A_F, C_tilde, G_z, 1e-8 * np.eye(2))
+ kalman = qe.Kalman(lss)
+ kalman.stationary_values()
+ V = kalman.stationary_innovation_covar()
+ C_wold = np.array(kalman.stationary_coefficients(T - 1, 'ma'))
+ A_var = np.array(kalman.stationary_coefficients(T - 1, 'var'))
+
+ return dict(D=D, Γ=Γ, V=V, C=C_wold, A_var=A_var)
+```
+
+We take $N = 2{,}000$ draws from the prior and also record the objects at the true parameter values.
+
+```{code-cell} python3
+rng = np.random.default_rng(1234)
+N = 2_000
+g_s_draws = rng.uniform(g_s_lo, g_s_hi, N)
+g_d_draws = rng.uniform(g_d_lo, g_d_hi, N)
+
+draws = [model_objects(gs, gd) for gs, gd in zip(g_s_draws, g_d_draws)]
+D_draws = np.array([d['D'] for d in draws]) # (N, T, 2, 2)
+Γ_draws = np.array([d['Γ'] for d in draws])
+V_draws = np.array([d['V'] for d in draws]) # (N, 2, 2)
+C_draws = np.array([d['C'] for d in draws]) # (N, T, 2, 2)
+A_var_draws = np.array([d['A_var'] for d in draws])
+
+truth = model_objects(g_s_true, g_d_true)
+```
+
+In every array of impulse responses, entry `[j, i, k]` is the response at horizon `j` of variable `i`
+($0 = q$, $1 = p$) to shock `k`.
+
+We'll use a helper that draws a band between the 5th and 95th percentiles together with the median.
+
+```{code-cell} python3
+def fan(ax, paths, color, label, x=None, alpha=0.25):
+ "Plot the 5-95 percentile band and the median of paths (draws x horizons)."
+ lo, med, hi = np.percentile(paths, [5, 50, 95], axis=0)
+ x = np.arange(paths.shape[1]) if x is None else x
+ ax.fill_between(x, lo, hi, color=color, alpha=alpha, lw=0)
+ ax.plot(x, med, color=color, lw=2, label=label)
+
+var_names = ["quantity $q$", "price $p$"]
+shock_names = ["supply shock $\\tilde w_s$", "demand shock $\\tilde w_d$"]
+var_short = ["q", "p"]
+```
+
+## Object 1: structural impulse responses
+
+The first figure shows the induced prior over the responses of $(q_t, p_t)$ to the two structural
+shocks.
+
+The dashed black lines are the responses at the true parameter values.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced prior over structural impulse responses
+ name: fig-ip-structural-irfs
+---
+fig, axes = plt.subplots(2, 2, figsize=(12, 7))
+for i in range(2):
+ for k in range(2):
+ ax = axes[i, k]
+ fan(ax, D_draws[:, :, i, k], 'C0', 'induced prior: median, 5-95%')
+ ax.plot(truth['D'][:, i, k], 'k--', lw=2, label='true parameters')
+ ax.axhline(0, color='k', lw=0.5)
+ ax.set_title(f"response of {var_names[i]} to {shock_names[k]}")
+ ax.set_xlabel("periods")
+axes[0, 0].legend()
+plt.tight_layout()
+plt.show()
+```
+
+Over the ranges spanned by the prior, the qualitative shapes of the structural responses do not
+change.
+
+A positive demand shock raises quantity for many periods and raises price for three periods, after
+which the price undershoots.
+
+A favorable supply shock raises quantity and lowers price on impact, but its effects are small and change
+sign after a couple of periods --- an imprint of the moving-average polynomial
+$B_s(L) = 1 - .2L - .4L^2 - .064 L^3$, whose negative lag coefficients reverse the cost shock.
+
+A uniform prior over a parameter generally induces a *non-uniform* prior over the responses.
+
+The histograms below show the induced priors over the four impact responses.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced prior over impact responses
+ name: fig-ip-impact-hist
+---
+fig, axes = plt.subplots(2, 2, figsize=(12, 6))
+for i in range(2):
+ for k in range(2):
+ ax = axes[i, k]
+ ax.hist(D_draws[:, 0, i, k], bins=40, density=True, color='C0', alpha=0.7)
+ ax.axvline(truth['D'][0, i, k], color='k', ls='--')
+ ax.set_title(f"impact response of {var_names[i]} to {shock_names[k]}")
+plt.tight_layout()
+plt.show()
+```
+
+Which uncertain parameter drives which response?
+
+The correlations of the impact responses with the parameter draws tell us.
+
+```{code-cell} python3
+for i in range(2):
+ for k in range(2):
+ x = D_draws[:, 0, i, k]
+ print(f"impact of {shock_names[k][:6]} on {var_short[i]}: "
+ f"corr with g_s = {np.corrcoef(g_s_draws, x)[0, 1]:6.3f}, "
+ f"corr with g_d = {np.corrcoef(g_d_draws, x)[0, 1]:6.3f}")
+```
+
+The response to the supply shock is governed almost entirely by the supplier's adjustment cost $g_s$: a
+larger $g_s$ makes the supplier less willing to change output, so a given cost reduction moves quantity
+less.
+
+The demander's $g_d$ matters mainly for the response of price to a demand shock.
+
+### The sign implications of the theory
+
+For each variable, shock, and horizon, we can compute the induced prior probability that the response
+is positive.
+
+When that probability is one, the sign is an implication of the theory that is robust to the modeler's
+uncertainty about $(g_s, g_d)$.
+
+```{code-cell} python3
+K_max = 5
+prob_pos = (D_draws[:, :K_max + 1] > 0).mean(axis=0) # (K_max+1, 2, 2)
+prob_neg = (D_draws[:, :K_max + 1] < 0).mean(axis=0)
+
+# +1: positive with prior probability one; -1: negative with probability one; 0: '?'
+sign_table = np.where(prob_pos == 1, 1, np.where(prob_neg == 1, -1, 0))
+
+symbol = {1: ' + ', -1: ' - ', 0: ' ? '}
+print("horizon | supply: q p | demand: q p")
+for j in range(K_max + 1):
+ s = [symbol[sign_table[j, i, k]] for k in range(2) for i in range(2)]
+ print(f" {j} | {s[0]} {s[1]} | {s[2]} {s[3]}")
+```
+
+On impact, the theory delivers exactly the sign pattern in Uhlig's static example: a favorable supply
+shock moves price and quantity in *opposite* directions, while a positive demand shock moves them in the
+*same* direction.
+
+The theory says more than that.
+
+It also signs most responses at longer horizons: the supply responses *reverse* sign from horizon 2 on,
+and the price response to a demand shock turns negative from horizon 3 on.
+
+At horizon 1 the supply responses are almost exactly zero and their signs depend on $(g_s, g_d)$, so the
+theory is silent there.
+
+```{code-cell} python3
+print("prior probability that the horizon-1 supply responses are positive:")
+print(f" q: {prob_pos[1, 0, 0]:.3f}, p: {prob_pos[1, 1, 0]:.3f}")
+```
+
+{cite:t}`LeeperTraumWalker2017` report tables of exactly this kind --- for
+example, the prior probability that a consumption multiplier is positive, horizon by horizon and model by
+model.
+
+They put entries of 0 or 1 to the opposite use from Uhlig.
+
+For Leeper, Traum, and Walker, such an entry is a warning that a model will deliver that sign no matter
+what the data say, so they prefer specifications whose induced priors leave signs open.
+
+For Uhlig, the same entry is a restriction that the theory licenses and that is worth imposing.
+
+## Object 2: autocovariances
+
+An econometrician with an infinitely long sample of $(q_t, p_t)$ could learn the autocovariance
+function $\{\Gamma_j\}$, and nothing more.
+
+The induced prior over the autocovariance function is therefore the prior over everything that data
+could ever reveal.
+
+We plot autocorrelations of $q$ and $p$ and the cross-correlation $\mathrm{corr}(q_{t+j}, p_t)$, using
+$\Gamma_{-j} = \Gamma_j^\top$ for negative $j$.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced prior over correlations
+ name: fig-ip-autocorr
+---
+sd = np.sqrt(np.stack([Γ_draws[:, 0, 0, 0], Γ_draws[:, 0, 1, 1]], axis=1)) # (N, 2)
+ρ_q = Γ_draws[:, :, 0, 0] / sd[:, [0]]**2
+ρ_p = Γ_draws[:, :, 1, 1] / sd[:, [1]]**2
+# corr(q_{t+j}, p_t) for j >= 0 and corr(q_{t-j}, p_t) = corr(p_{t+j}, q_t) for j > 0
+cross_pos = Γ_draws[:, :, 0, 1] / (sd[:, [0]] * sd[:, [1]])
+cross_neg = Γ_draws[:, 1:, 1, 0] / (sd[:, [0]] * sd[:, [1]])
+cross = np.hstack([cross_neg[:, ::-1], cross_pos])
+lags = np.arange(-(T - 1), T)
+
+fig, axes = plt.subplots(1, 3, figsize=(14, 4))
+fan(axes[0], ρ_q, 'C0', 'induced prior')
+axes[0].set_title("autocorrelation of $q$")
+fan(axes[1], ρ_p, 'C1', 'induced prior')
+axes[1].set_title("autocorrelation of $p$")
+fan(axes[2], cross, 'C2', 'induced prior', x=lags)
+axes[2].set_title("cross-correlation corr$(q_{t+j}, p_t)$")
+for ax in axes:
+ ax.axhline(0, color='k', lw=0.5)
+ ax.set_xlabel("$j$")
+plt.tight_layout()
+plt.show()
+```
+
+Quantity is much more persistent than price, an outcome of the large supply-side adjustment cost.
+
+Quantity and price are strongly positively correlated contemporaneously, and price leads quantity.
+
+## Object 3: the population VAR
+
+By Wold's theorem, $z_t$ has a vector autoregression
+
+$$
+z_t = \sum_{j=1}^{\infty} A_j z_{t-j} + a_t, \qquad \mathbb{E}[a_t a_t^\top] = V,
+$$
+
+with moving-average representation $z_t = \sum_{j \ge 0} C_j a_{t-j}$, $C_0 = I$.
+
+The VAR coefficients $A_j$, the innovation covariance $V$, and the Wold coefficients $C_j$ are all
+functions of the autocovariance function alone.
+
+So the prior over $(g_s, g_d)$ also induces a prior over the reduced-form VAR, an idea that
+{cite:t}`DelNegroSchorfheide2004` used to construct priors for VARs from equilibrium
+models.
+
+The VAR has infinitely many lags, but its coefficients decay quickly.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Size of the VAR coefficients
+ name: fig-ip-var-size
+---
+size = np.abs(A_var_draws).max(axis=(2, 3)) # largest |entry| of A_j, lag by lag
+fig, ax = plt.subplots()
+fan(ax, np.log10(size), 'C0', 'induced prior', x=np.arange(1, T + 1))
+ax.set_xlabel("lag $j$")
+ax.set_ylabel("$\\log_{10} \\max |A_j|$")
+plt.tight_layout()
+plt.show()
+```
+
+### The innovation covariance matrix
+
+Next we look at the induced prior over the three distinct elements of $V$, expressed as two standard
+deviations and a correlation.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced prior over innovation moments
+ name: fig-ip-innov
+---
+sd_a = np.sqrt(np.stack([V_draws[:, 0, 0], V_draws[:, 1, 1]], axis=1))
+corr_a = V_draws[:, 0, 1] / (sd_a[:, 0] * sd_a[:, 1])
+V_true = truth['V']
+
+fig, axes = plt.subplots(1, 3, figsize=(14, 3.5))
+axes[0].hist(sd_a[:, 0], bins=40, density=True, color='C0', alpha=0.7)
+axes[0].axvline(np.sqrt(V_true[0, 0]), color='k', ls='--')
+axes[0].set_title("std. dev. of the $q$ innovation")
+axes[1].hist(sd_a[:, 1], bins=40, density=True, color='C1', alpha=0.7)
+axes[1].axvline(np.sqrt(V_true[1, 1]), color='k', ls='--')
+axes[1].set_title("std. dev. of the $p$ innovation")
+axes[2].hist(corr_a, bins=40, density=True, color='C2', alpha=0.7)
+axes[2].axvline(V_true[0, 1] / np.sqrt(V_true[0, 0] * V_true[1, 1]),
+ color='k', ls='--')
+axes[2].set_title("correlation of the innovations")
+plt.tight_layout()
+plt.show()
+```
+
+The innovations to quantity and price are almost perfectly correlated for every parameter value in the
+prior's support.
+
+```{code-cell} python3
+print(f"range of corr(a_q, a_p) over the prior draws: "
+ f"[{corr_a.min():.4f}, {corr_a.max():.4f}]")
+```
+
+Demand shocks have a much larger variance than supply shocks and move $q$ and $p$ in the same
+direction, so they dominate the one-step-ahead prediction errors.
+
+{cite:t}`Uhlig2017` shows that in a static market a nearly perfect positive price–quantity
+correlation is the case in which sign restrictions are most informative; we'll return to that point
+below.
+
+How does $V$ compare with the covariance $D_0 D_0^\top$ of the agents' own one-step-ahead surprises
+$D_0 \epsilon_t$?
+
+In {doc}`supply_demand_var` we saw that the econometrician's innovations have the larger covariance,
+because the agents' shocks are not fundamental for $(q_t, p_t)$.
+
+```{code-cell} python3
+Σ_struct = np.einsum('nij,nkj->nik', D_draws[:, 0], D_draws[:, 0]) # D_0 D_0'
+gap = V_draws - Σ_struct
+print("eigenvalues of V - D_0 D_0' are nonnegative in every draw:",
+ np.all(np.linalg.eigvalsh(gap) > -1e-10))
+for i, name in enumerate(["q", "p"]):
+ rel = gap[:, i, i] / V_draws[:, i, i]
+ print(f"(V - D_0 D_0')_{name}{name} / V_{name}{name}: "
+ f"median {np.median(rel):.4f}, range [{rel.min():.4f}, {rel.max():.4f}]")
+```
+
+The gap is small relative to $V$ --- less than one percent of the variance of the quantity innovation.
+
+But we'll see that the gap is large relative to the contribution of the *supply* shock, and that is what
+matters for recovering the supply shock from a VAR.
+
+### Two Cholesky orthogonalizations
+
+The most common way to turn VAR innovations into "structural" shocks is a recursive orthogonalization.
+
+* **$q$ first.** Factor $V = L_q L_q^\top$ with $L_q$ lower triangular. The first orthogonalized shock is
+ the innovation in $q_t$; the second is the part of the innovation in $p_t$ orthogonal to it, and it does
+ not move $q_t$ on impact.
+* **$p$ first.** Reorder the variables as $(p_t, q_t)$ and factor. The first shock is the innovation in
+ $p_t$; the second is the part of the innovation in $q_t$ orthogonal to it, and it does not move $p_t$ on
+ impact.
+
+In each case the impulse responses are $C_j L$ for the corresponding impact matrix $L$.
+
+We normalize signs as Uhlig does, so that each shock raises quantity on impact; when a shock does not
+move quantity on impact, we normalize it so that it *lowers* price, as a favorable supply shock does.
+
+```{code-cell} python3
+P_swap = np.array([[0, 1], [1, 0]])
+
+def normalize(A):
+ "Flip columns so that q rises on impact, or, if q does not move, p falls."
+ A = A.copy()
+ for k in range(A.shape[1]):
+ key = A[0, k] if abs(A[0, k]) > 1e-10 else -A[1, k]
+ A[:, k] *= np.sign(key)
+ return A
+
+def cholesky_impact(V, first):
+ "Impact matrix in (q, p) order; column 0 is the shock ordered first."
+ if first == 'q':
+ L = np.linalg.cholesky(V)
+ else:
+ L = P_swap @ np.linalg.cholesky(P_swap @ V @ P_swap)
+ return normalize(L)
+
+chol_irfs = {}
+for first in ['q', 'p']:
+ L_draws = np.array([cholesky_impact(V, first) for V in V_draws])
+ chol_irfs[first] = np.einsum('ntij,njk->ntik', C_draws, L_draws)
+
+chol_true = {first: truth['C'] @ cholesky_impact(truth['V'], first)
+ for first in ['q', 'p']}
+```
+
+Because demand shocks dominate both innovations, the shock ordered *first* in either ordering is mostly
+a demand shock, and the shock ordered *second* is the natural candidate for a supply shock.
+
+We therefore compare the first Cholesky shock with the demand shock and the second Cholesky shock with
+the supply shock.
+
+```{code-cell} python3
+def compare_plot(first):
+ fig, axes = plt.subplots(2, 2, figsize=(12, 7))
+ pairs = [(1, 0), (0, 1)] # (structural shock, Cholesky shock)
+ other = 'p' if first == 'q' else 'q'
+ chol_names = [f"innovation in ${first}$ (ordered first)",
+ f"${other}$ innovation orthogonal to ${first}$ (ordered second)"]
+ for col, (k_struct, k_chol) in enumerate(pairs):
+ for i in range(2):
+ ax = axes[i, col]
+ fan(ax, D_draws[:, :, i, k_struct], 'gray',
+ f"structural: {shock_names[k_struct]}")
+ fan(ax, chol_irfs[first][:, :, i, k_chol], 'C3',
+ f"VAR, {first} first: {chol_names[k_chol]}")
+ ax.axhline(0, color='k', lw=0.5)
+ ax.set_title(f"response of {var_names[i]}")
+ ax.set_xlabel("periods")
+ axes[0, col].legend(fontsize=8)
+ plt.tight_layout()
+ plt.show()
+```
+
+First the ordering that puts $q_t$ first.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Cholesky ($q$ first) versus structural responses
+ name: fig-ip-chol-q
+---
+compare_plot('q')
+```
+
+Then the ordering that puts $p_t$ first.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Cholesky ($p$ first) versus structural responses
+ name: fig-ip-chol-p
+---
+compare_plot('p')
+```
+
+The left columns show that, in both orderings, the first Cholesky shock nearly reproduces the response to
+the demand shock.
+
+The induced prior bands are almost indistinguishable --- the $p$-first ordering does especially well,
+because the price innovation is almost entirely a demand surprise.
+
+The right columns tell a different story.
+
+Neither ordering produces anything that resembles the response to a supply shock.
+
+* With $q$ first, the second shock does not move quantity on impact --- by construction --- while a
+ supply shock raises it; the second shock lowers price on impact by roughly nine times as much as a supply
+ shock does, and its quantity trough is about twice as deep.
+* With $p$ first, the second shock does not move price on impact --- again by construction --- while a
+ supply shock lowers it; the second shock overstates the impact on quantity, and its quantity response
+ misses the sign reversal that the structural response displays from horizon 2 on.
+
+These discrepancies hold for every parameter value in the prior's support: both induced priors are
+concentrated, but they are concentrated in different places.
+
+The numbers at the true parameters confirm this.
+
+```{code-cell} python3
+np.set_printoptions(precision=4, suppress=True)
+print("impact responses (rows q, p) at the true parameters\n")
+print("structural (supply, demand):\n", truth['D'][0])
+print("\nq-first Cholesky (first, second):\n", chol_true['q'][0])
+print("\np-first Cholesky (first, second):\n", chol_true['p'][0])
+```
+
+## Can any rotation recover the structural shocks?
+
+A Cholesky factorization is just one rotation.
+
+Could *some* rotation $R$ of the VAR innovations recover the structural impulse responses?
+
+Uhlig's Proposition A.1 {cite}`Uhlig2005` says that a vector $d \in \mathbb{R}^2$ is an impulse vector
+of the VAR --- a column of some $A$ with $A A^\top = V$ --- if and only if $d = L r$ with $\|r\| = 1$,
+that is, if and only if
+
+$$
+\| L^{-1} d \| = 1 .
+$$
+
+So we can ask, draw by draw, whether the structural impact vectors --- the columns $d_s, d_d$ of $D_0$
+--- are admissible impulse vectors.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced prior over $\|L^{-1} d\|$
+ name: fig-ip-impulse-norms
+---
+L_q_draws = np.linalg.cholesky(V_draws)
+norms = np.linalg.norm(np.linalg.solve(L_q_draws, D_draws[:, 0]), axis=1) # (N, 2)
+
+fig, axes = plt.subplots(1, 2, figsize=(12, 3.5))
+for k, ax in enumerate(axes):
+ ax.hist(norms[:, k], bins=40, density=True, color=f'C{k}', alpha=0.7)
+ ax.set_title(f"$\\|L^{{-1}} d\\|$ for the {shock_names[k]}")
+plt.tight_layout()
+plt.show()
+
+for k in range(2):
+ print(f"{shock_names[k][:6]}: ||L^-1 d|| in "
+ f"[{norms[:, k].min():.4f}, {norms[:, k].max():.4f}]")
+```
+
+For the demand shock the norm is one, up to the accuracy of our computations: the demand shock is
+(almost) fundamental for $(q_t, p_t)$, and a suitable rotation of the VAR innovations recovers it.
+
+For the supply shock the norm lies between $0.80$ and $0.87$ throughout the support of the prior.
+
+No rotation of the VAR innovations can deliver the structural supply impact vector, because that vector
+is "too short".
+
+The reason is the one identified in {doc}`supply_demand_var`: because quantity adjusts sluggishly, a supply
+surprise is revealed to an econometrician only gradually, so the one-step-ahead VAR innovation contains
+only part of it, mixed with old news.
+
+The structural moving average $z_t = \sum_j D_j \epsilon_{t-j}$ and the Wold moving average
+$z_t = \sum_j C_j a_{t-j}$ imply the same autocovariances, but $\{D_j\}$ is not of the form $\{C_j L R\}$
+for any orthogonal $R$.
+
+## Sign restrictions motivated by theory
+
+Now consider a second econometrician who has an infinite sample from the economy at the true parameter
+values, and so knows $V$ and $\{C_j\}$ exactly.
+
+This econometrician is not willing to impose the whole structural model, but accepts the sign
+restrictions that the theory delivers with prior probability one --- the table that we computed above.
+
+Following Uhlig's Principles 1 and 2, the econometrician imposes what is known and nothing more.
+
+The econometrician applies Uhlig's draw-and-reject algorithm:
+
+1. draw $r$ uniformly from the unit circle and form the orthogonal matrix $R = [\,r,\ r^\perp\,]$, where
+ $r^\perp$ is $r$ rotated by ninety degrees;
+2. compute the impact matrix $A = L R$, flipping the sign of each column so that quantity rises on
+ impact, and the impulse responses
+ $C_j A$, treating the first column as the supply shock and the second as the demand shock;
+3. keep the draw if the responses satisfy every restriction in the table at horizons
+ $j = 0, \ldots, K$;
+4. repeat many times.
+
+Because $r$ ranges over the whole circle and signs are normalized afterwards, this procedure visits every
+orthogonal $R$, including those that swap the roles of the two columns.
+
+Because the likelihood is flat over the admissible rotations in an infinite sample, the kept draws
+form both the identified set and, with the Haar prior, the econometrician's posterior (Uhlig's Principle 9).
+
+```{code-cell} python3
+def sign_identified(C, V, table, K, n_draws=20_000, seed=0):
+ """
+ Uhlig's draw-and-reject algorithm.
+
+ Returns the impulse responses (draws, T, 2, 2) of the kept rotations,
+ with shock 0 = supply and shock 1 = demand, and the acceptance rate.
+ """
+ rng = np.random.default_rng(seed)
+ L = np.linalg.cholesky(V)
+ x = rng.standard_normal((n_draws, 2))
+ r = x / np.linalg.norm(x, axis=1, keepdims=True) # uniform on the circle
+ R = np.stack([np.stack([r[:, 0], r[:, 1]], 1),
+ np.stack([-r[:, 1], r[:, 0]], 1)], axis=2) # columns r, r_perp
+ A = L @ R
+ A = A * np.sign(A[:, [0], :]) # q rises on impact
+ irf = np.einsum('tij,njk->ntik', C, A) # (n_draws, T, 2, 2)
+
+ ok = np.all(irf[:, :K + 1] * table[:K + 1] >= 0, axis=(1, 2, 3))
+ return irf[ok], ok.mean()
+```
+
+We impose the restrictions on impact only ($K = 0$) and for six periods ($K = 5$).
+
+```{code-cell} python3
+sets = {}
+for K in [0, K_max]:
+ sets[K], acc = sign_identified(truth['C'], truth['V'], sign_table, K)
+ print(f"K = {K}: acceptance rate {acc:.4f}")
+```
+
+Most draws are rejected.
+
+That is good news, not bad: as Uhlig's Principle 7 puts it, when a lot of draws are rejected,
+identification is sharp.
+
+We plot the identified sets as the range between the smallest and largest response at each horizon, as in
+Figure 2 of {cite:t}`Uhlig2005`, and compare them with the true structural responses and the $p$-first
+Cholesky responses.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Sign-identified sets at the true parameters
+ name: fig-ip-sign-sets
+---
+fig, axes = plt.subplots(2, 2, figsize=(12, 7))
+p_first_pair = {0: 1, 1: 0} # structural shock -> p-first Cholesky column
+for k in range(2):
+ for i in range(2):
+ ax = axes[i, k]
+ for K, color in [(0, 'C0'), (K_max, 'C3')]:
+ s = sets[K][:, :, i, k]
+ ax.fill_between(range(T), s.min(0), s.max(0), color=color,
+ alpha=0.3, lw=0, label=f"sign-identified set, K = {K}")
+ ax.plot(truth['D'][:, i, k], 'k--', lw=2, label='true structural response')
+ ax.plot(chol_true['p'][:, i, p_first_pair[k]], 'C2', lw=2,
+ label='p-first Cholesky')
+ ax.axhline(0, color='k', lw=0.5)
+ ax.set_title(f"response of {var_names[i]} to {shock_names[k]}")
+ ax.set_xlabel("periods")
+ ax.set_xlim(0, 12)
+axes[0, 0].legend(fontsize=8)
+plt.tight_layout()
+plt.show()
+```
+
+The demand shock is identified almost exactly.
+
+As Uhlig's analysis of a static market predicts, the near-perfect positive correlation of the price and
+quantity innovations leaves very few rotations that move price and quantity in opposite directions for
+one shock and in the same direction for the other.
+
+The supply shock is a different matter.
+
+With restrictions on impact only, the identified set is wide: on impact the price response can be
+anywhere from zero to almost nine times its true value.
+
+Imposing the restrictions for $K = 5$ periods --- restrictions that are *correct*, since the theory implies
+them for every parameter value in the prior's support --- narrows the set dramatically.
+
+But the narrower set *excludes* the true response: the true response of quantity lies outside it at
+every horizon, and the true response of price from horizon 2 on.
+
+One edge of the $K = 5$ set is the $p$-first Cholesky response, which satisfies the restriction on the
+impact response of price only weakly, with a response of exactly zero.
+
+```{code-cell} python3
+s5 = sets[K_max][:, :, :, 0]
+inside = ((s5.min(0) <= truth['D'][:, :, 0] + 1e-12)
+ & (truth['D'][:, :, 0] <= s5.max(0) + 1e-12))
+print("is the true supply response inside the K = 5 set?")
+print("horizon: ", np.arange(8))
+print("quantity: ", inside[:8, 0])
+print("price: ", inside[:8, 1])
+```
+
+The explanation is the non-fundamentalness that we diagnosed above.
+
+The sign restrictions are true statements about the structural responses $D_j$.
+
+But the econometrician can only search over responses of the form $C_j L r$, and the true supply response
+is not among them.
+
+The sign restrictions pick out the rotations that best mimic the signs of a supply shock, and those rotations
+get the magnitudes wrong.
+
+Sign restrictions are a way to deal with an identification problem *within* the set of rotations of the
+VAR innovations; they offer no protection against the possibility that the shocks of interest are not
+rotations of the VAR innovations at all.
+
+{cite:t}`Uhlig2017` explicitly sets aside "the thorny issue of fundamentalness"; our market shows
+why it deserves attention.
+
+### Robustness across the prior
+
+Is the exclusion of the truth special to $(g_s, g_d) = (10, 0.1)$?
+
+We repeat the exercise for a few hundred draws from the prior, treating each in turn as the truth, and
+record whether the true impact response of quantity to a supply shock lies in the $K = 5$ identified set.
+
+```{code-cell} python3
+n_check = 300
+covered = np.empty(n_check, dtype=bool)
+for n in range(n_check):
+ kept, _ = sign_identified(C_draws[n], V_draws[n], sign_table, K_max,
+ n_draws=5_000, seed=n)
+ lo, hi = kept[:, 0, 0, 0].min(), kept[:, 0, 0, 0].max()
+ covered[n] = lo <= D_draws[n, 0, 0, 0] <= hi
+print(f"fraction of draws in which the K = {K_max} set contains the true "
+ f"impact response of q to a supply shock: {covered.mean():.3f}")
+```
+
+Throughout the support of the prior, the theory-motivated sign restrictions identify a set that misses
+the true response.
+
+## Widening the prior
+
+So far the modeler has been fairly confident: $g_s$ is known to within $\pm 20$ percent and $g_d$ to within
+$\pm 50$ percent.
+
+What happens to the induced priors when the modeler is less sure?
+
+We compare the baseline prior with two wider uniform priors.
+
+```{code-cell} python3
+priors = {
+ 'baseline': ((g_s_lo, g_s_hi), (g_d_lo, g_d_hi)),
+ 'wider': ((4.0, 20.0), (0.02, 0.3)),
+ 'widest': ((1.5, 40.0), (0.005, 1.0)),
+}
+
+N_wide = 1_500
+wide = {'baseline': dict(g_s=g_s_draws, g_d=g_d_draws,
+ D=D_draws, V=V_draws, C=C_draws)}
+for name in ['wider', 'widest']:
+ (gs_lo, gs_hi), (gd_lo, gd_hi) = priors[name]
+ rng_w = np.random.default_rng(2024)
+ gs = rng_w.uniform(gs_lo, gs_hi, N_wide)
+ gd = rng_w.uniform(gd_lo, gd_hi, N_wide)
+ objs = [model_objects(x, y) for x, y in zip(gs, gd)]
+ wide[name] = dict(g_s=gs, g_d=gd,
+ D=np.array([o['D'] for o in objs]),
+ V=np.array([o['V'] for o in objs]),
+ C=np.array([o['C'] for o in objs]))
+```
+
+### Bigger induced priors
+
+The figure overlays the three induced priors over the structural impulse responses.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Induced priors under wider priors
+ name: fig-ip-wide-irfs
+---
+colors = {'baseline': 'C0', 'wider': 'C1', 'widest': 'C3'}
+
+fig, axes = plt.subplots(2, 2, figsize=(12, 7))
+for name in ['widest', 'wider', 'baseline']:
+ (gs_lo, gs_hi), (gd_lo, gd_hi) = priors[name]
+ label = f"$g_s \\sim U[{gs_lo:g}, {gs_hi:g}]$, $g_d \\sim U[{gd_lo:g}, {gd_hi:g}]$"
+ for i in range(2):
+ for k in range(2):
+ fan(axes[i, k], wide[name]['D'][:, :, i, k], colors[name], label,
+ alpha=0.35)
+for i in range(2):
+ for k in range(2):
+ axes[i, k].axhline(0, color='k', lw=0.5)
+ axes[i, k].set_title(f"response of {var_names[i]} to {shock_names[k]}")
+ axes[i, k].set_xlabel("periods")
+ axes[i, k].set_xlim(0, 12)
+axes[0, 0].legend(fontsize=8)
+plt.tight_layout()
+plt.show()
+```
+
+The bands fan out as the prior widens, most of all for the impact responses to a supply shock and for
+the response of quantity to a demand shock.
+
+Two features are worth noticing.
+
+First, the induced prior moves as well as spreads: under the widest prior the median response of
+quantity to a demand shock is far below the baseline median.
+
+A uniform prior on $[1.5, 40]$ puts most of its mass on large values of $g_s$, at which suppliers adjust
+output very slowly, so "uniform" is far from "uninformative" about impulse responses (we return to this
+point {ref}`below `).
+
+Second, some signs become uncertain: under the widest prior the response of quantity to a demand shock
+can turn negative after a few periods.
+
+To see how the parameters map into responses, we plot the impact responses for each draw from the
+widest prior, coloring each point by its value of $g_s$.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Impact responses under the widest prior
+ name: fig-ip-wide-scatter
+---
+D_w, gs_w, gd_w = wide['widest']['D'], wide['widest']['g_s'], wide['widest']['g_d']
+
+fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
+for k, ax in enumerate(axes):
+ sc = ax.scatter(D_w[:, 0, 0, k], D_w[:, 0, 1, k], c=gs_w, s=6, cmap='plasma')
+ ax.set_xlabel("impact response of $q$")
+ ax.set_ylabel("impact response of $p$")
+ ax.set_title(f"impact responses to the {shock_names[k]}")
+fig.colorbar(sc, ax=axes, label="$g_s$")
+plt.show()
+```
+
+Each point is one economy.
+
+A larger supply-side adjustment cost $g_s$ shrinks the quantity response to *both* shocks, and the
+uniform prior's concentration on large $g_s$ piles the draws up in the upper-left corner of each cloud.
+
+### Where the supply shock is fundamental
+
+The same draws let us map where the supply shock can be recovered from a VAR.
+
+We plot $\|L^{-1} d_s\|$ against $g_s$ and color each point by $g_d$.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Where the supply shock is fundamental
+ name: fig-ip-fundamental
+---
+V_w = wide['widest']['V']
+norms_w = np.linalg.norm(np.linalg.solve(np.linalg.cholesky(V_w), D_w[:, 0]), axis=1)
+
+fig, ax = plt.subplots()
+sc = ax.scatter(gs_w, norms_w[:, 0], c=gd_w, s=6, cmap='viridis')
+ax.axhline(1, color='k', lw=0.5)
+ax.set_xlabel("$g_s$")
+ax.set_ylabel("$\\|L^{-1} d_s\\|$")
+fig.colorbar(sc, ax=ax, label="$g_d$")
+plt.tight_layout()
+plt.show()
+```
+
+For small $g_s$, $\|L^{-1} d_s\| = 1$: quantity adjusts quickly enough that a VAR innovation reveals
+the supply surprise at once, and the supply shock is fundamental for $(q_t, p_t)$.
+
+Beyond a threshold that rises with $g_d$, the norm falls below one and keeps falling as $g_s$ grows.
+
+The baseline prior $g_s \in [8, 12]$ sits well inside the non-fundamental region.
+
+### What sign restrictions deliver under a wider prior
+
+A wider prior has a cost for the sign-restriction approach: fewer signs hold for *every* parameter value,
+so the theory licenses fewer restrictions.
+
+```{code-cell} python3
+def theory_signs(D, K=K_max):
+ "Signs that hold with prior probability one: +1, -1, or 0 for '?'."
+ pos = (D[:, :K + 1] > 0).mean(axis=0)
+ neg = (D[:, :K + 1] < 0).mean(axis=0)
+ return np.where(pos == 1, 1, np.where(neg == 1, -1, 0))
+
+table_w = theory_signs(D_w)
+print("signs implied by the widest prior")
+print("horizon | supply: q p | demand: q p")
+for j in range(K_max + 1):
+ s = [symbol[table_w[j, i, k]] for k in range(2) for i in range(2)]
+ print(f" {j} | {s[0]} {s[1]} | {s[2]} {s[3]}")
+```
+
+Compared with the baseline table, several entries have become "?".
+
+Among the restrictions that survive is that a favorable supply shock *raises* price two periods later,
+the reversal that the cost-shock process $B_s(L)$ builds in.
+
+We now treat each draw from the widest prior in turn as the truth, give the econometrician its population
+VAR, impose the table above for $K = 5$, and record one of three outcomes:
+
+* the sign-identified set contains the true impact response of quantity to a supply shock,
+* the set is nonempty but misses it, or
+* no rotation satisfies the restrictions at all.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Sign-restriction outcomes, uniform prior
+ name: fig-ip-outcomes-uniform
+---
+def classify(C, V, D, table, K=K_max):
+ """
+ For each economy: 0 if no rotation is admissible, 1 if the identified set
+ misses the true impact response of q to a supply shock, 2 if it contains it.
+ """
+ out = np.empty(len(D), dtype=int)
+ for n in range(len(D)):
+ kept, _ = sign_identified(C[n], V[n], table, K, n_draws=10_000, seed=n)
+ if len(kept) == 0:
+ out[n] = 0
+ else:
+ lo, hi = kept[:, 0, 0, 0].min(), kept[:, 0, 0, 0].max()
+ out[n] = 2 if lo <= D[n, 0, 0, 0] <= hi else 1
+ return out
+
+outcome_labels = ["no admissible rotation", "set misses the truth",
+ "set contains the truth"]
+outcome_colors = ['C7', 'C3', 'C2']
+
+def outcome_map(ax, g_s, g_d, outcome, title=None):
+ for code in range(3):
+ m = outcome == code
+ ax.scatter(g_s[m], g_d[m], s=8, color=outcome_colors[code],
+ label=f"{outcome_labels[code]} ({m.mean():.0%})")
+ ax.set_xlabel("$g_s$")
+ ax.set_ylabel("$g_d$")
+ if title is not None:
+ ax.set_title(title)
+ ax.legend(loc='upper right', fontsize=9)
+
+C_w = wide['widest']['C']
+outcome = classify(C_w, V_w, D_w, table_w)
+
+fig, ax = plt.subplots()
+outcome_map(ax, gs_w, gd_w, outcome)
+plt.tight_layout()
+plt.show()
+```
+
+The parameter space splits into three regions.
+
+* For small $g_s$ (green), the restrictions work as intended: the identified set contains the truth.
+ This region includes the one where the supply shock is fundamental, and extends somewhat beyond it
+ because the widest prior licenses fewer restrictions, which leaves wider identified sets.
+* For intermediate $g_s$ (red), the set is nonempty but, as at our baseline parameters, it misses the
+ truth.
+* For large $g_s$ (gray), *no* rotation of the VAR innovations satisfies the restrictions.
+
+In the gray region, an econometrician who applies Uhlig's procedure to the population VAR finds that the
+sign restrictions are inconsistent with the data and would reject the theory that implied them --- even
+though every restriction is a true statement about how the economy responds to its shocks.
+
+Which restriction cannot be met?
+
+We drop the horizon-2 restriction on the price response to a supply shock and try again on the gray
+draws.
+
+```{code-cell} python3
+table_drop = table_w.copy()
+table_drop[2, 1, 0] = 0 # horizon 2, price, supply shock
+gray = np.where(outcome == 0)[0]
+nonempty = [len(sign_identified(C_w[n], V_w[n], table_drop, K_max,
+ n_draws=10_000, seed=n)[0]) > 0 for n in gray]
+print(f"gray draws with a nonempty set after dropping that restriction: "
+ f"{np.mean(nonempty):.0%}")
+```
+
+The culprit is the delayed rise in price after a favorable supply shock.
+
+When suppliers adjust slowly, the VAR innovations reveal that part of the supply shock's propagation only
+gradually, so no rotation of them can reproduce it while also matching the signs on impact.
+
+The lesson is that a broader prior can't rescue the sign-restriction approach here.
+
+Fewer restrictions survive, and for a large part of the prior's support the restrictions that survive
+either miss the truth or cannot be satisfied at all.
+
+(ip_log_uniform)=
+### A log-uniform prior on $g_s$
+
+The widest prior spans more than an order of magnitude, $g_s \in [1.5, 40]$.
+
+For a parameter like this --- a positive scale parameter whose plausible values range over several
+multiples --- a uniform prior is a strong opinion: it puts 65 percent of its mass on $[15, 40]$ and less
+than 7 percent on $[1.5, 4]$, even though each interval spans the same *proportional* range, a factor of
+$8/3$.
+
+A natural alternative is a **log-uniform** prior, under which $\log g_s$ is uniform on
+$[\log 1.5, \log 40]$, so that every doubling of $g_s$ receives the same prior probability.
+
+Its density is proportional to $1/g_s$ on the support.
+
+We keep the uniform prior on $g_d$ and the same supports, and change only the *shape* of the prior on
+$g_s$.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Draws of $g_s$ under two priors
+ name: fig-ip-gs-draws
+---
+(gs_lo, gs_hi), (gd_lo, gd_hi) = priors['widest']
+rng_log = np.random.default_rng(2024)
+gs_log = np.exp(rng_log.uniform(np.log(gs_lo), np.log(gs_hi), N_wide))
+gd_log = rng_log.uniform(gd_lo, gd_hi, N_wide)
+objs = [model_objects(x, y) for x, y in zip(gs_log, gd_log)]
+wide['log-uniform'] = dict(g_s=gs_log, g_d=gd_log,
+ D=np.array([o['D'] for o in objs]),
+ V=np.array([o['V'] for o in objs]),
+ C=np.array([o['C'] for o in objs]))
+
+gs_ticks = [2, 5, 10, 20, 40]
+
+fig, ax = plt.subplots()
+bins = np.geomspace(gs_lo, gs_hi, 30) # equally spaced in log g_s
+w = np.full(N_wide, 1 / N_wide)
+ax.hist(gs_w, bins=bins, weights=w, alpha=0.5, color='C3', label='uniform')
+ax.hist(gs_log, bins=bins, weights=w, alpha=0.5, color='C4', label='log-uniform')
+ax.set_xscale('log')
+ax.set_xticks(gs_ticks, labels=[str(t) for t in gs_ticks])
+ax.set_xlabel("$g_s$ (log scale)")
+ax.set_ylabel("fraction of draws")
+ax.legend()
+plt.tight_layout()
+plt.show()
+
+for name, gs in [('uniform', gs_w), ('log-uniform', gs_log)]:
+ print(f"{name:12s}: median g_s = {np.median(gs):5.2f}, "
+ f"P(g_s < 4) = {np.mean(gs < 4):.2f}, P(g_s > 15) = {np.mean(gs > 15):.2f}")
+```
+
+On a logarithmic axis the log-uniform draws are spread evenly, while the uniform draws pile up at the
+right.
+
+The induced priors over impulse responses change accordingly.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Uniform versus log-uniform prior on $g_s$
+ name: fig-ip-log-irfs
+---
+D_log = wide['log-uniform']['D']
+spec_colors = {'uniform': 'C3', 'log-uniform': 'C4'}
+
+fig, axes = plt.subplots(2, 2, figsize=(12, 7))
+for name, D_ in [('uniform', D_w), ('log-uniform', D_log)]:
+ for i in range(2):
+ for k in range(2):
+ fan(axes[i, k], D_[:, :, i, k], spec_colors[name],
+ f"{name} prior on $g_s \\in [1.5, 40]$", alpha=0.3)
+for i in range(2):
+ for k in range(2):
+ axes[i, k].axhline(0, color='k', lw=0.5)
+ axes[i, k].set_title(f"response of {var_names[i]} to {shock_names[k]}")
+ axes[i, k].set_xlabel("periods")
+ axes[i, k].set_xlim(0, 12)
+axes[0, 0].legend(fontsize=8)
+plt.tight_layout()
+plt.show()
+```
+
+Moving prior mass toward small adjustment costs raises the median quantity responses to both shocks and
+widens their bands upward: economies in which suppliers adjust quickly now carry real prior weight.
+
+What about the sign restrictions?
+
+A sign holds with prior probability one if and only if it holds at every point of the support, apart
+from a set of prior probability zero.
+
+Because both priors have positive density everywhere on the same support, they imply the *same* sign
+restrictions.
+
+```{code-cell} python3
+table_log = theory_signs(D_log)
+print("same sign table under the uniform and log-uniform priors:",
+ np.array_equal(table_log, table_w))
+```
+
+So the econometrician who imposes the theory's sign restrictions does exactly the same thing under the two
+priors, and the map from $(g_s, g_d)$ to the three outcomes is the same.
+
+What changes is how much prior probability falls in each region.
+
+```{code-cell} python3
+---
+mystnb:
+ figure:
+ caption: Sign-restriction outcomes, log-uniform prior
+ name: fig-ip-outcomes-log
+---
+V_log, C_log = wide['log-uniform']['V'], wide['log-uniform']['C']
+outcome_log = classify(C_log, V_log, D_log, table_log)
+
+fig, ax = plt.subplots()
+outcome_map(ax, gs_log, gd_log, outcome_log)
+ax.set_xscale('log')
+ax.set_xticks(gs_ticks, labels=[str(t) for t in gs_ticks])
+ax.set_xlabel("$g_s$ (log scale)")
+plt.tight_layout()
+plt.show()
+
+norms_log = np.linalg.norm(
+ np.linalg.solve(np.linalg.cholesky(V_log), D_log[:, 0]), axis=1)
+print(f"{'':12s} {'P(fundamental)':>15s}" +
+ ''.join(f"{lab:>25s}" for lab in outcome_labels))
+for name, nrm, out in [('uniform', norms_w, outcome),
+ ('log-uniform', norms_log, outcome_log)]:
+ print(f"{name:12s} {np.mean(nrm[:, 0] > 0.99):15.2f}" +
+ ''.join(f"{np.mean(out == c):25.2f}" for c in range(3)))
+```
+
+Here `P(fundamental)` is the prior probability $\mathbb{P}\{\|L^{-1} d_s\| > 0.99\}$.
+
+On a logarithmic axis the three regions are laid out evenly, and the green region --- where the
+theory-implied sign restrictions recover the true supply response --- is now the largest.
+
+The same sign restrictions, derived from the same theory with the same parameter support, succeed with
+prior probability of roughly one quarter under one prior and about 60 percent under the other.
+
+This complements a point that {cite:t}`BaumeisterHamilton2015` make about the
+Haar prior over rotations: a prior that looks innocuous on one parameterization can be quite informative
+about the objects we care about.
+
+Here the choice is between two equally "flat-looking" priors on a *structural* parameter, and it governs
+how likely we think it is that a VAR can recover the supply shock at all.
+
+## Summary
+
+A Bayesian structural modeler who is uncertain about two adjustment-cost parameters has, implicitly, a
+prior over every object that the model implies.
+
+Monte Carlo makes those induced priors visible.
+
+In our market:
+
+* The induced prior over **structural impulse responses** is concentrated and pins down a rich pattern of
+ signs, many of which persist beyond the impact period.
+* The induced prior over the **population VAR** says that the innovations to price and quantity are almost
+ perfectly correlated, with a covariance matrix slightly larger than that of the agents' own
+ surprises.
+* Under either **Cholesky ordering**, the first orthogonalized shock looks like the demand shock, but no
+ orthogonalized shock looks like the supply shock.
+* Using the induced prior to generate **sign restrictions** in the spirit of Uhlig works well for the
+ demand shock, which is essentially fundamental for $(q_t, p_t)$, and fails for the supply shock, which
+ is not: imposing more correct restrictions shrinks the identified set onto the wrong answer.
+* **Widening the prior** produces bigger, and shifted, induced priors and weakens the restrictions the
+ theory can license. Across the widest prior, sign restrictions recover the supply response only where
+ suppliers adjust relatively quickly, so that the supply shock is fundamental or close to it; elsewhere
+ the identified set misses the truth or is empty.
+* **Changing the shape of the prior** from uniform to log-uniform on the same support leaves the
+ theory-implied sign restrictions unchanged --- they depend only on the support --- but moves prior mass
+ toward fast-adjusting suppliers, and so raises the prior probability that those restrictions recover the
+ supply response from roughly one quarter to about 60 percent.
+
+The structural modeler, who believes the whole model, can do better.
+
+With an infinite sample, the modeler can learn $(g_s, g_d)$ from the autocovariance function (see
+{ref}`ip_ex3`) and then compute the structural responses directly from the model, without ever
+rotating VAR innovations: *if you know it, impose it*.
+
+## Exercises
+
+```{exercise}
+:label: ip_ex1
+
+{ref}`Exercise 2 ` of {doc}`supply_demand_var` showed that the market is (nearly) invertible when $g_s \le 1$:
+quantity then adjusts quickly and reveals supply surprises at once.
+
+Replace the prior on $g_s$ by $g_s \sim U[0.2, 1]$, keeping $g_d \sim U[0.05, 0.15]$.
+
+1. Recompute the induced prior over $\|L^{-1} d_s\|$.
+2. Recompute the sign table implied by the new prior.
+3. Take $(g_s, g_d) = (0.6, 0.1)$ as the truth and apply Uhlig's algorithm with the new table and
+ $K = 5$. Does the identified set now contain the true supply response?
+```
+
+```{solution-start} ip_ex1
+:class: dropdown
+```
+
+We repeat the Monte Carlo with the new prior.
+
+```{code-cell} python3
+rng_ex = np.random.default_rng(42)
+N_ex = 1_000
+gs_ex = rng_ex.uniform(0.2, 1.0, N_ex)
+gd_ex = rng_ex.uniform(g_d_lo, g_d_hi, N_ex)
+draws_ex = [model_objects(gs, gd) for gs, gd in zip(gs_ex, gd_ex)]
+D_ex = np.array([d['D'] for d in draws_ex])
+V_ex = np.array([d['V'] for d in draws_ex])
+
+norms_ex = np.linalg.norm(
+ np.linalg.solve(np.linalg.cholesky(V_ex), D_ex[:, 0]), axis=1)
+print(f"||L^-1 d_s|| in [{norms_ex[:, 0].min():.4f}, {norms_ex[:, 0].max():.4f}]")
+
+pos_ex = (D_ex[:, :K_max + 1] > 0).mean(axis=0)
+neg_ex = (D_ex[:, :K_max + 1] < 0).mean(axis=0)
+table_ex = np.where(pos_ex == 1, 1, np.where(neg_ex == 1, -1, 0))
+print("\nhorizon | supply: q p | demand: q p")
+for j in range(K_max + 1):
+ s = [symbol[table_ex[j, i, k]] for k in range(2) for i in range(2)]
+ print(f" {j} | {s[0]} {s[1]} | {s[2]} {s[3]}")
+```
+
+The supply impact vector is now an admissible impulse vector, $\|L^{-1} d_s\| \approx 1$.
+
+Now we apply the sign restrictions at $(g_s, g_d) = (0.6, 0.1)$.
+
+```{code-cell} python3
+truth_ex = model_objects(0.6, 0.1)
+kept_ex, acc_ex = sign_identified(truth_ex['C'], truth_ex['V'], table_ex, K_max)
+print(f"acceptance rate: {acc_ex:.4f}")
+
+lo, hi = kept_ex.min(0), kept_ex.max(0)
+inside_ex = (lo - 1e-10 <= truth_ex['D']) & (truth_ex['D'] <= hi + 1e-10)
+print("true supply response inside the set, horizons 0-7:")
+print(" quantity:", inside_ex[:8, 0, 0])
+print(" price: ", inside_ex[:8, 1, 0])
+
+fig, axes = plt.subplots(1, 2, figsize=(12, 4))
+for i, ax in enumerate(axes):
+ ax.fill_between(range(T), lo[:, i, 0], hi[:, i, 0], color='C3', alpha=0.3,
+ label=f'sign-identified set, K = {K_max}')
+ ax.plot(truth_ex['D'][:, i, 0], 'k--', lw=2, label='true structural response')
+ ax.axhline(0, color='k', lw=0.5)
+ ax.set_title(f"response of {var_names[i]} to the supply shock, $g_s = 0.6$")
+ ax.set_xlim(0, 12)
+axes[0].legend()
+plt.tight_layout()
+plt.show()
+```
+
+When the structural shocks are fundamental for $(q_t, p_t)$, the true structural responses are among the
+rotations of the VAR innovations, so correct sign restrictions produce an identified set that contains
+them.
+
+```{solution-end}
+```
+
+```{exercise}
+:label: ip_ex2
+
+Uhlig's Principle 6 {cite}`Uhlig2017` says that sign-restricting both shocks rather than one "can help:
+sometimes a lot, sometimes not at all".
+
+At the true parameters $(g_s, g_d) = (10, 0.1)$ and with $K = 0$, compare the identified sets for the
+impact responses when you impose
+
+1. only the restrictions on the supply shock,
+2. only the restrictions on the demand shock,
+3. both.
+
+Which restriction does the work, and why?
+```
+
+```{solution-start} ip_ex2
+:class: dropdown
+```
+
+We zero out the rows of the sign table that we don't want to impose.
+
+```{code-cell} python3
+cases = {"supply only": [0], "demand only": [1], "both": [0, 1]}
+for name, shocks in cases.items():
+ tab = np.zeros_like(sign_table)
+ tab[:, :, shocks] = sign_table[:, :, shocks]
+ kept, acc = sign_identified(truth['C'], truth['V'], tab, 0)
+ bounds = ([kept[:, 0, i, k].min() for k in range(2) for i in range(2)],
+ [kept[:, 0, i, k].max() for k in range(2) for i in range(2)])
+ print(f"{name:12s} acceptance {acc:.3f}")
+ for (i, k), lo_, hi_ in zip([(0, 0), (1, 0), (0, 1), (1, 1)], *bounds):
+ print(f" impact of {shock_names[k][:6]} on {var_short[i]}: "
+ f"[{lo_:7.4f}, {hi_:7.4f}] truth {truth['D'][0, i, k]:7.4f}")
+```
+
+The supply restriction does almost all of the work.
+
+Because the innovations are almost perfectly positively correlated, the data cloud lies close to a line with
+positive slope.
+
+Only rotations that put the "supply" impulse vector nearly orthogonal to that line can make price and
+quantity move in opposite directions, and that forces the other impulse vector to lie almost along the
+line, which pins down the demand shock.
+
+The demand restriction alone has little bite: almost every direction in the positive quadrant is consistent
+with it.
+
+This is the case $\mathrm{corr}(P, Q) \to 1$ in Figure 7 of {cite:t}`Uhlig2017`.
+
+```{solution-end}
+```
+
+```{exercise}
+:label: ip_ex3
+
+The structural modeler believes the whole model.
+
+Suppose the modeler sees an infinite sample from the economy at $(g_s, g_d) = (10, 0.1)$, and therefore
+knows the autocovariances $\Gamma_0, \ldots, \Gamma_{T-1}$.
+
+On a grid of values $(g_s, g_d)$ covering the support of the prior, compute the distance
+
+$$
+\sum_{j=0}^{T-1} \| \Gamma_j(g_s, g_d) - \Gamma_j(10, 0.1) \|^2
+$$
+
+and show that it has a unique minimum at the true values.
+
+What does this imply about the modeler's posterior in an infinite sample, and about the posterior over the
+structural impulse responses?
+```
+
+```{solution-start} ip_ex3
+:class: dropdown
+```
+
+```{code-cell} python3
+gs_grid = np.linspace(g_s_lo, g_s_hi, 21)
+gd_grid = np.linspace(g_d_lo, g_d_hi, 21)
+Γ_target = truth['Γ']
+
+dist = np.empty((len(gs_grid), len(gd_grid)))
+for m, gs in enumerate(gs_grid):
+ for n, gd in enumerate(gd_grid):
+ dist[m, n] = np.sum((model_objects(gs, gd)['Γ'] - Γ_target)**2)
+
+m, n = np.unravel_index(dist.argmin(), dist.shape)
+print(f"minimizer on the grid: g_s = {gs_grid[m]:.2f}, g_d = {gd_grid[n]:.3f}")
+
+fig, ax = plt.subplots(figsize=(7, 5))
+cs = ax.contourf(gd_grid, gs_grid, np.log10(np.maximum(dist, 1e-6)), levels=30)
+ax.plot(g_d_true, g_s_true, 'w*', ms=14)
+ax.set_xlabel("$g_d$")
+ax.set_ylabel("$g_s$")
+ax.set_title("$\\log_{10}$ distance between autocovariance functions")
+fig.colorbar(cs)
+plt.tight_layout()
+plt.show()
+```
+
+The autocovariance function identifies $(g_s, g_d)$.
+
+The elongated valley shows that the autocovariances are less sensitive to movements in which $g_s$ and
+$g_d$ rise together than to movements in which they go in opposite directions, so a finite sample would
+leave the modeler with a positively correlated posterior.
+
+In an infinite sample the modeler's likelihood concentrates on the true values, so the posterior over
+$(g_s, g_d)$ collapses to a point mass at $(10, 0.1)$, and so does the posterior over the structural
+impulse responses, since they are a function of $(g_s, g_d)$.
+
+The modeler learns the true supply response exactly, including the part that no rotation of VAR
+innovations can deliver, because the modeler does not use VAR rotations: the model maps the
+parameters directly into the responses.
+
+This is the payoff from Uhlig's first principle, *if you know it, impose it*.
+
+The sign-restriction econometrician, who imposes less, learns less, and in this market learns something
+wrong about the supply shock.
+
+```{solution-end}
+```
+
+```{exercise}
+:label: ip_ex4
+
+The widest prior on $g_d$, $U[0.005, 1]$, spans an even wider range, a factor of 200.
+
+Put log-uniform priors on *both* $g_s \in [1.5, 40]$ and $g_d \in [0.005, 1]$.
+
+1. Check that the theory-implied sign table is unchanged.
+2. Compute the prior probabilities of the three sign-restriction outcomes and compare them with those
+ under the uniform prior and under the prior that is log-uniform in $g_s$ only.
+3. Explain the direction of the change, using the outcome maps above.
+```
+
+```{solution-start} ip_ex4
+:class: dropdown
+```
+
+```{code-cell} python3
+rng_ll = np.random.default_rng(99)
+gs_ll = np.exp(rng_ll.uniform(np.log(1.5), np.log(40.0), N_wide))
+gd_ll = np.exp(rng_ll.uniform(np.log(0.005), np.log(1.0), N_wide))
+objs = [model_objects(x, y) for x, y in zip(gs_ll, gd_ll)]
+D_ll = np.array([o['D'] for o in objs])
+V_ll = np.array([o['V'] for o in objs])
+C_ll = np.array([o['C'] for o in objs])
+
+print("same sign table:", np.array_equal(theory_signs(D_ll), table_w))
+outcome_ll = classify(C_ll, V_ll, D_ll, table_w)
+
+print(f"\n{'':22s}" + ''.join(f"{lab:>25s}" for lab in outcome_labels))
+for name, out in [('uniform, uniform', outcome),
+ ('log-uniform, uniform', outcome_log),
+ ('log-uniform, log-unif.', outcome_ll)]:
+ print(f"{name:22s}" + ''.join(f"{np.mean(out == c):25.2f}" for c in range(3)))
+
+fig, ax = plt.subplots(figsize=(9, 5))
+outcome_map(ax, gs_ll, gd_ll, outcome_ll,
+ "log-uniform priors on $g_s$ and $g_d$: sign restrictions, K = 5")
+ax.set_xscale('log')
+ax.set_yscale('log')
+ax.set_xticks(gs_ticks, labels=[str(t) for t in gs_ticks])
+ax.set_xlabel("$g_s$ (log scale)")
+ax.set_ylabel("$g_d$ (log scale)")
+plt.tight_layout()
+plt.show()
+```
+
+The sign table is unchanged because the support is unchanged.
+
+A log-uniform prior on $g_d$ shifts mass toward *small* values of $g_d$.
+
+The outcome maps show that the green region extends to larger values of $g_s$ when
+$g_d$ is large: the boundary between the green and red regions slopes upward.
+
+Moving prior mass toward small $g_d$ therefore moves mass out of the green region, and the probability that
+the sign restrictions recover the supply response falls relative to the prior that is log-uniform in $g_s$
+alone, though it stays well above its value under the uniform prior.
+
+```{solution-end}
+```