Skip to content

Commit f476022

Browse files
committed
Enhance Hessian handling and improve DSGE model representation
- Added `hessian_strategy` parameter to `run_from_config` and `run_map` functions to allow selection between optimizer and numerical Hessian methods. - Introduced `build_proposal_from_covariance` function for covariance-based proposal generation. - Updated `DSGE` class string representation to improve readability and formatting. - Enhanced LaTeX representation of the DSGE model to ensure clarity in output. - Added tests to verify the correct LaTeX representation of the DSGE model.
1 parent c1756c9 commit f476022

8 files changed

Lines changed: 123 additions & 46 deletions

File tree

configs/default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ map:
1111
method: L-BFGS-B
1212
hess_step: 0.0001
1313
tau_scale: 0.5
14+
hessian_strategy: auto
1415
include_jacobian_prior: false
1516

1617
mcmc:

configs/nk_full_yaml.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ map:
131131
bounds: auto
132132
include_jacobian_prior: false
133133
hess_step: 0.0001
134-
tau_scale: 0.3
134+
tau_scale: 0.25
135+
hessian_strategy: auto
135136

136137
mcmc:
137138
enabled: false

configs/tiny_ar1.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ map:
5858
include_jacobian_prior: false
5959
hess_step: 0.0001
6060
tau_scale: 0.4
61+
hessian_strategy: auto
6162

6263
mcmc:
6364
enabled: false

notebooks/showcase_dsge_octave.ipynb

Lines changed: 26 additions & 18 deletions
Large diffs are not rendered by default.

scripts/run_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def run_from_config(config_path: Path, *, dry_run: bool = False) -> dict[str, An
130130
"method": map_cfg.get("method", "L-BFGS-B"),
131131
"hess_step": float(map_cfg.get("hess_step", 1e-4)),
132132
"tau_scale": float(map_cfg.get("tau_scale", 0.5)),
133+
"hessian_strategy": map_cfg.get("hessian_strategy", "auto"),
133134
"include_jacobian_prior": bool(map_cfg.get("include_jacobian_prior", False)),
134135
},
135136
run_mcmc=bool(mcmc_cfg.get("enabled", False)),

src/dgse.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ class ModelSignature:
3333
n_shocks: int
3434

3535
def __str__(self) -> str:
36+
def _label(n: int, singular: str, plural: str) -> str:
37+
return f"{n} {singular if n == 1 else plural}"
38+
3639
return (
37-
f"{self.n_equations} eqs | "
38-
f"{self.n_states} states "
39-
f"(+{self.n_leads} leads, {self.n_lags} lags) | "
40-
f"{self.n_shocks} shocks")
40+
f"{_label(self.n_equations, 'equation', 'equations')} | "
41+
f"{_label(self.n_states, 'state', 'states')} "
42+
f"({_label(self.n_leads, 'lead', 'leads')}, "
43+
f"{_label(self.n_lags, 'lag', 'lags')}) | "
44+
f"{_label(self.n_shocks, 'shock', 'shocks')}")
4145

4246

4347
def _escape_latex(text: str) -> str:
@@ -174,19 +178,12 @@ def _repr_latex_(self) -> str:
174178
def _esc(x):
175179
return _escape_latex(str(x))
176180

177-
sig = _esc(getattr(self, "signature", ""))
181+
sig = str(self.signature).replace("(", "| ").replace(", ", " | ").replace(")", "")
178182

179183
meta = ""
180184
if getattr(self, "_metadata", None):
181185
items = [f"{_esc(k)}={_esc(v)}" for k, v in self._metadata.items()]
182-
meta = r"\quad\text{\small(" + ", ".join(items) + ")}"
183-
184-
header = (
185-
r"{\normalsize \mathbf{DSGE\ Model}}"
186-
r"\\[2pt]"
187-
r"{\scriptsize " + sig + r"}"
188-
+ meta
189-
+ r"\\[4pt]")
186+
meta = r"\\[2pt]{\scriptsize\text{" + ", ".join(items) + r"}}"
190187

191188
rows = []
192189
for idx, eq in enumerate(self._equations, start=1):
@@ -200,12 +197,16 @@ def _esc(x):
200197
body = "\n".join(rows)
201198

202199
latex = (
203-
r"\begin{aligned}"
204-
+ header
200+
r"\begin{array}{c}"
201+
r"{\large\mathbf{DSGE\ Model}}"
202+
r"\\[4pt]"
203+
r"{\small\text{" + _esc(sig) + r"}}"
204+
+ meta
205+
+ r"\\[8pt]"
205206
+ r"\left\{\,\begin{array}{rcll}"
206207
+ body
207208
+ r"\end{array}\right."
208-
+ r"\end{aligned}")
209+
+ r"\end{array}")
209210
return latex
210211

211212

@@ -303,7 +304,8 @@ def compute(
303304
allowed_map_keys = {
304305
"method",
305306
"hess_step",
306-
"tau_scale",}
307+
"tau_scale",
308+
"hessian_strategy",}
307309

308310
filtered_map_kwargs = {k: v for k, v in map_kwargs.items() if k in allowed_map_keys}
309311

src/inference/map.py

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def numerical_hessian_central(f, x, h=1e-3, ridge=1e-6):
6363
H = 0.5*(H + H.T) + ridge*np.eye(n)
6464
return H
6565

66-
def build_proposal_from_hessian(H, tau_scale=0.5, min_eig=1e-8):
66+
def build_proposal_from_hessian(H, tau_scale=0.5, min_eig=1e-8):
6767
"""
6868
Toma H ~ ∇²(-logpost)(MAP).
6969
Devuelve (cov_prop, chol_prop).
@@ -94,7 +94,34 @@ def build_proposal_from_hessian(H, tau_scale=0.5, min_eig=1e-8):
9494
cov_spd_j = cov_spd + jitter*np.eye(cov_spd.shape[0])
9595
L = np.linalg.cholesky(cov_spd_j)
9696

97-
return cov_spd, L
97+
return cov_spd, L
98+
99+
100+
def build_proposal_from_covariance(cov, tau_scale=0.5, min_eig=1e-8):
101+
cov = np.asarray(cov, dtype=float)
102+
cov = 0.5 * (cov + cov.T)
103+
cov_spd = make_spd_eig(tau_scale * cov, min_eig)
104+
try:
105+
L = np.linalg.cholesky(cov_spd)
106+
except np.linalg.LinAlgError:
107+
L = np.linalg.cholesky(cov_spd + 1e-6 * np.eye(cov_spd.shape[0]))
108+
return cov_spd, L
109+
110+
111+
def _optimizer_inverse_hessian(res, n_params: int):
112+
hess_inv = getattr(res, "hess_inv", None)
113+
if hess_inv is None:
114+
return None
115+
try:
116+
if hasattr(hess_inv, "todense"):
117+
H_inv = np.asarray(hess_inv.todense(), dtype=float)
118+
else:
119+
H_inv = np.asarray(hess_inv, dtype=float)
120+
except Exception:
121+
return None
122+
if H_inv.shape != (n_params, n_params) or not np.all(np.isfinite(H_inv)):
123+
return None
124+
return 0.5 * (H_inv + H_inv.T)
98125

99126

100127
def run_map(
@@ -114,6 +141,7 @@ def run_map(
114141
method: str = "L-BFGS-B",
115142
hess_step: float = 1e-4,
116143
tau_scale: float = 0.5,
144+
hessian_strategy: str = "auto",
117145
include_jacobian_prior: bool = False , div = 0,
118146
likelihood_kwargs: Optional[Dict] = None):
119147

@@ -147,19 +175,41 @@ def f_obj(theta_work: np.ndarray):
147175
include_jacobian=include_jacobian_prior,
148176
likelihood_kwargs=likelihood_kwargs)
149177

150-
res = minimize(f_obj, theta0, method=method, bounds=bounds)
151-
152-
theta_map = np.asarray(res.x, dtype=float)
153-
H = numerical_hessian_central(f_obj, theta_map, h=hess_step)
154-
cov_prop, chol_prop = build_proposal_from_hessian(H, tau_scale=tau_scale, min_eig=1e-8)
178+
res = minimize(f_obj, theta0, method=method, bounds=bounds)
179+
180+
theta_map = np.asarray(res.x, dtype=float)
181+
strategy = hessian_strategy.lower()
182+
if strategy not in {"auto", "optimizer", "numerical"}:
183+
raise ValueError("hessian_strategy debe ser 'auto', 'optimizer' o 'numerical'.")
184+
185+
H_inv_opt = None
186+
if strategy in {"auto", "optimizer"}:
187+
H_inv_opt = _optimizer_inverse_hessian(res, theta_map.size)
188+
189+
if H_inv_opt is not None:
190+
cov_prop, chol_prop = build_proposal_from_covariance(
191+
H_inv_opt,
192+
tau_scale=tau_scale,
193+
min_eig=1e-8,
194+
)
195+
try:
196+
H = np.linalg.pinv(H_inv_opt)
197+
except np.linalg.LinAlgError:
198+
H = np.eye(theta_map.size)
199+
hessian_source = "optimizer"
200+
else:
201+
H = numerical_hessian_central(f_obj, theta_map, h=hess_step)
202+
cov_prop, chol_prop = build_proposal_from_hessian(H, tau_scale=tau_scale, min_eig=1e-8)
203+
hessian_source = "numerical"
155204

156205

157206
return {
158207
"theta_map": theta_map,
159208
"neglogpost_map": float(res.fun),
160209
"success": bool(res.success),
161210
"message": res.message,
162-
"nit": res.nit,
163-
"hessian": H,
164-
"cov_proposal": cov_prop,
211+
"nit": res.nit,
212+
"hessian": H,
213+
"hessian_source": hessian_source,
214+
"cov_proposal": cov_prop,
165215
"chol_proposal": chol_prop}

tests/test_public_imports.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,16 @@ def test_dsge_compatibility_import():
22
from src.dsge import DSGE
33

44
assert DSGE.__name__ == "DSGE"
5+
6+
7+
def test_dsge_latex_repr_has_readable_signature():
8+
import sympy as sp
9+
from src.dsge import DSGE
10+
11+
x_t, eps_t = sp.symbols("x_t eps_t")
12+
model = DSGE([sp.Eq(x_t, eps_t)], [x_t], eps_t=[eps_t])
13+
14+
latex = model._repr_latex_()
15+
16+
assert "1 equation | 1 state | 0 leads | 0 lags | 1 shock" in latex
17+
assert "1eqs" not in latex

0 commit comments

Comments
 (0)