Spec 0.1eA: Monolithic dolo-plus → vanilla Dolo horse (semiconjugate solve)¶
Version: 0.1eA Date: 2026-02-11 Status: Under development Author: Akshay Shanker
0. Purpose¶
Spec 0.1b (spec_0.1b-doloplus-to-dolo-translator.md) delivers a single-stage dolo-plus → Dolo translator that emits a dtcc/EGM horse.
This spec 0.1eA describes how to solve a finite-horizon consumption problem by:
- translating a materialized dolo-plus stage into a vanilla Dolo horse via
doloplus_to_dolo, - feeding the horse directly to Dolo's
egm()solver, and - recovering the modular \(a\)-space iterates via a final expectation step.
The mathematical justification is the order semiconjugate transform (Stachurski 2009, 2022): the two orderings of the sub-operators \(\mathbf{E}\) (expectation) and \(\mathbf{M}\) (maximisation) yield semiconjugate dynamical systems. This allows us to iterate the monolithic operator \(\hat{\mathbf{S}} = \mathbf{M} \circ \mathbf{E}\) inside Dolo's EGM solver and translate the result back to the modular operator \(\mathbf{S} = \mathbf{E} \circ \mathbf{M}\) via a single final \(\mathbf{E}\) step.
See Solving via Conjugation for the full derivation.
1. The semiconjugate relationship¶
The noport–cons period decomposes the Bellman operator into two sub-operators:
Two period operators arise from composing these in opposite order:
| Operator | Definition | Domain | Role |
|---|---|---|---|
| \(\mathbf{S} = \mathbf{E} \circ \mathbf{M}\) | Problem B (modular) | \(a\)-space | optimise, then integrate |
| \(\hat{\mathbf{S}} = \mathbf{M} \circ \mathbf{E}\) | Problem A (monolithic) | \(m\)-space | integrate, then optimise |
These are order semiconjugate under \(\mathbf{F} = \mathbf{M}\) and \(\mathbf{G} = \mathbf{E}\). The iterates lemma gives:
Reading right-to-left: apply \(\mathbf{M}\) once to the terminal value, iterate \(\hat{\mathbf{S}}\) for \(n-1\) steps, then apply one final \(\mathbf{E}\).
2. The monolithic horse¶
2.1 What the horse provides¶
The monolithic horse is a single vanilla Dolo dtcc YAML that egm() can swallow directly. It implements \(\hat{\mathbf{S}} = \mathbf{M} \circ \mathbf{E}\) — each EGM iteration first integrates (\(\mathbf{E}\)), then optimises (\(\mathbf{M}\)).
From packages/dolo/dolo/algos/egm.py, the solver loads six gufunctions:
funs = model.__original_gufunctions__
h = funs["expectation"] # h(M, S, X, p) → z
gt = funs["half_transition"] # gt(m, a, M, p) → S
τ = funs["direct_response_egm"] # τ(m, a, z, p) → X
aτ = funs["reverse_state"] # aτ(m, a, X, p) → S
lb = funs["arbitrage_lb"] # lb(m, S, p) → lower bound
ub = funs["arbitrage_ub"] # ub(m, S, p) → upper bound
The backward-iteration loop is:
for each exogenous node i_m:
m = dp.node(i_m)
for each future node i_M:
w = dp.iweight(i_m, i_M)
M = dp.inode(i_m, i_M)
S = gt(m, a, M, p) ← half_transition
X = drfut(i_M, S) ← interpolate future policy on S
z += w * h(M, S, X, p) ← expectation integrand
xa = τ(m, a, z, p) ← direct_response_egm (inverse Euler)
sa = aτ(m, a, xa, p) ← reverse_state (endogenous grid)
2.2 Mapping: dolo-plus stages → dtcc blocks¶
The composed horse assembles blocks from both the noport and cons stages:
| Source stage | dolo-plus equation | dtcc block | Equation |
|---|---|---|---|
| noport | dcsn_to_cntn_transition (collapsed with identity arvl_to_dcsn) |
half_transition |
m[t] = a[t-1]*R + θ[t] |
| cons | cntn_to_dcsn_mover.InvEuler |
direct_response_egm |
c[t] = (β*mr[t])^(-1/ρ) |
| cons | dcsn_to_cntn_transition |
auxiliary_direct_egm |
a[t] = m[t] - c[t] |
| cons | cntn_to_dcsn_transition |
reverse_state |
m[t] = a[t] + c[t] |
| noport+cons | synthesized from T_ed.ShadowBellman × T_da.ShadowBellman |
expectation |
mr[t] = (c[t+1])^(-ρ)*R |
| cons | bounds from constraints | arbitrage |
0 \| 0.0<=c[t]<=m[t] |
The expectation synthesis composes a marginal-utility integrand from the consumption stage (e.g. \(u'(c)=c^{-\rho}\)), shifts the integrand to [t+1] so that egm() evaluates it at the future decision rule, and multiplies by any required non-discount factors (e.g. \(R\)). This is already implemented via EXPECTATION_CONFIG in explore/transformations/egm-ADC-to-dolo/mapping_tables.yaml.
Convention note (β placement). You can place \(\beta\) either inside
expectation:or insidedirect_response_egm:. The mapping tables in this repo currently place \(\beta\) indirect_response_egm:(coming fromInvEuler) and leave it out ofexpectation:.
2.3 Expected horse YAML¶
The output from the translator (or an equivalent hand-written reference model) looks like:
name: noport_cons_period
symbols:
exogenous: [θ]
states: [m]
poststates: [a]
controls: [c]
expectations: [mr]
parameters: [β, ρ, R, μ_θ, σ_θ]
equations:
half_transition: |
m[t] = a[t-1]*R + θ[t]
auxiliary_direct_egm: |
a[t] = m[t] - c[t]
reverse_state: |
m[t] = a[t] + c[t]
expectation: |
mr[t] = ( c[t+1] )^(-ρ)*R
direct_response_egm: |
c[t] = (β*mr[t])^(-1/ρ)
arbitrage: |
0 | 0.0<=c[t]<=m[t]
calibration:
β: 0.96
ρ: 2.0
R: 1.02
μ_θ: 0.0
σ_θ: 0.10
m: 1.0
c: 0.8*m
domain:
m: [0.01, 10.0]
exogenous: !LogNormal
μ: μ_θ
σ: σ_θ
options:
grid: !Cartesian
orders: [100]
bounds: [[0.01, 10.0]]
This has the same dtcc block signature as packages/dolo/examples/models/consumption_savings_iid_egm.yaml. (The exact equations may differ by harmless conventions such as whether \(\beta\) lives in expectation: or in direct_response_egm:.)
3. Solving the finite-horizon problem¶
3.1 The iterates formula¶
For \(n\) steps back from the terminal period with terminal asset-space value \(w_H\):
3.2 Pseudocode (four steps)¶
Step 1: Translate dolo-plus → vanilla Dolo horse¶
The translator doloplus_to_dolo converts a materialized dolo-plus stage YAML into a vanilla Dolo dtcc model using mapping tables:
from dolo.compiler.model_import import yaml_import
from dolo.compiler.doloplus_translator import doloplus_to_dolo
model_plus = yaml_import(
"docs/old/examples/old/example-dolo-plus-consind-egm.yml",
compile_functions=False,
)
model = doloplus_to_dolo(
model_plus,
mapping_tables="explore/transformations/egm-ADC-to-dolo/mapping_tables.yaml",
compile_functions=True,
)
Alternatively, use the existing native horse directly:
Step 2: Provide the terminal decision rule dr0¶
For the no-bequest terminal condition \(w_H(a) = 0\), the terminal maximisation is "consume all": \(c(m) = m\). In Dolo, this is a decision rule passed as dr0:
This plays the role of \(\mathbf{M}\, w_H\) in the iterates formula.
Step 3: Iterate \(\hat{\mathbf{S}}^{H-1}\) with Dolo's EGM solver¶
Force exactly \(H - 1\) iterations by setting η_tol=0:
from dolo.algos.egm import egm
import numpy as np
H = 50
a_grid = np.linspace(0.01, 10.0, 100)
sol = egm(model, dr0=dr0, a_grid=a_grid, maxit=H-1, η_tol=0.0, verbose=True)
After \(H - 1\) iterations, sol.dr contains the \(m\)-space policies from \(\hat{\mathbf{S}}^{H-1} \circ \mathbf{M}\, w_H\).
Step 4: Final \(\mathbf{E}\) — recover the \(a\)-space marginal return mr(a)¶
The final \(\mathbf{E}\) is the expectation-accumulation half of the EGM loop, run once:
import numpy as np
funs = model.__original_gufunctions__
gt = funs["half_transition"]
h = funs["expectation"]
dp = sol.dprocess
p = model.calibration["parameters"]
a = a_grid[:, None]
i_m = 0
m = dp.node(i_m)
mr = np.zeros((len(a_grid), 1))
for i_M in range(dp.n_inodes(i_m)):
w = dp.iweight(i_m, i_M)
M = dp.inode(i_m, i_M)
S = gt(m, a, M, p)
X = sol.dr(i_M, S)
mr += w * h(M, S, X, p)
This produces mr(a), the expected marginal return on the asset grid — exactly the \(\mathbf{E}\)-image needed to complete \(\mathbf{S}^n = \mathbf{E} \circ \hat{\mathbf{S}}^{n-1} \circ \mathbf{M}\).
4. dolo-plus → Dolo mapping elements¶
4.1 Canonical dolo-plus elements¶
Each dolo-plus stage specifies:
- Perches / timing:
_arvl,_dcsn,_cntn - Transitions:
- \(\mathrm{g}_{\prec\sim}\):
arvl_to_dcsn_transition - \(\mathrm{g}_{\sim\succ}\):
dcsn_to_cntn_transition - \(\mathrm{g}_{\sim\succ}^{-1}\):
cntn_to_dcsn_transition(reverse, for EGM) - Movers:
- \(\mathbb{B}\):
cntn_to_dcsn_mover(includesInvEuler,ShadowBellman, etc.) - \(\mathbb{I}\):
dcsn_to_arvl_mover
4.2 Vanilla Dolo dtcc/EGM blocks¶
Dolo compiles a fixed menu of blocks (from packages/dolo/dolo/compiler/recipes.yaml):
half_transitionauxiliary_direct_egmreverse_stateexpectationdirect_response_egmarbitrage
4.3 Mapping table (composed horse)¶
The existing mapping (implemented in 0.1b + explore/transformations/egm-ADC-to-dolo/mapping_tables.yaml):
| dolo-plus canonical | Example label | Vanilla Dolo block |
|---|---|---|
g_ad |
arvl_to_dcsn_transition |
half_transition |
g_de |
dcsn_to_cntn_transition |
auxiliary_direct_egm |
g_ed |
cntn_to_dcsn_transition |
reverse_state |
T_ed.InvEuler |
cntn_to_dcsn_mover.InvEuler |
direct_response_egm |
T_ed.ShadowBellman + T_da.ShadowBellman |
mover synthesis | expectation |
4.4 Translation constraints¶
Non-ad hoc translator
Translation must be ruleset-driven (mapping tables + Dolang+ parsing), not model-specific Python:
- Parse via Dolang+:
yaml_import(..., compile_functions=False)is the input boundary. - Translate via mapping tables: all behaviour must be expressible in
mapping_tables.yaml. - Fail fast: missing canonical metadata or ambiguous timing must error.
4.5 Required dolo-plus metadata¶
Each dolo-plus stage file must include:
- A
dolo_plusheader:{dialect: adc-stage, version: 0.1} - A total
equation_symbolsmapping (label → canonical symbol) - Perch index aliases (if deviating from defaults)
4.6 Materializing calibration and settings¶
To emit a solvable horse YAML, the pipeline must include:
calibration:with parameter valuesdomain:andoptions:for grid configurationexogenous:with the shock distribution
These can come from a materialized dolo-plus source (like docs/old/examples/old/example-dolo-plus-consind-egm.yml) or be supplied by the builder.
5. Implementation¶
5.1 Artifacts¶
| File | Role |
|---|---|
explore/transformations/egm-ADC-to-dolo/mapping_tables.yaml |
Mapping-table ruleset |
explore/transformations/demo_transformation.py |
Translator demo (dolo-plus → Dolo YAML) |
explore/egm_cons_stage_pipeline.ipynb |
Existing pipeline notebook (parse → methodize → calibrate → configure) |
explore/semiconjugate_solve.ipynb |
New: full solve notebook (build → solve → plot) |
explore/semiconjugate_solve.py |
Mirror .py script of the solve notebook |
5.2 New notebook: semiconjugate_solve.ipynb¶
A clean, legible notebook that mirrors the pseudocode in §3.2 and Solving via Conjugation §4. The notebook is structured accretively: first build the model objects, then solve for a given horizon \(H\).
Cell structure (each major step is a separate cell or cell group):
Part A — Build¶
-
Setup + imports. Add repo packages to path; import
yaml_import,egm,numpy,matplotlib. -
Load the horse. Either:
- (a) translate from dolo-plus via
doloplus_to_dolo(shows the full pipeline), or - (b) load the native horse directly from
consumption_savings_iid_egm.yaml.
Both paths produce the same model object. Use (b) as the default for simplicity; show (a) in a commented or toggled cell so the translation path is documented.
- Set parameters. Horizon \(H\), asset grid
a_grid, terminal conditiondr0 = lambda i, s: s.
Part B — Solve¶
-
Run
egm(). Callegm(model, dr0=dr0, a_grid=a_grid, maxit=H-1, η_tol=0.0, verbose=True). Print iteration progress. This producessol.dr— the \(m\)-space policies after \(\hat{\mathbf{S}}^{H-1} \circ \mathbf{M}\, w_H\). -
Final \(\mathbf{E}\) step. Extract compiled gufunctions (
half_transition,expectation) and run the expectation-accumulation loop once to recovermr(a)on the asset grid.
Part C — Inspect¶
-
Plot consumption policies. Plot \(c(m)\) at selected horizons (e.g. \(h = 1, 5, 10, 25, 49\)) on a single figure. Verify: concave, increasing in \(m\), converging as \(H\) grows.
-
Plot
mr(a). Plot the expected marginal return on the asset grid — the \(\mathbf{E}\)-image from Step 5.
Design principles¶
- Accretive: the notebook builds the model in Part A, then solves in Part B. Parts are separable — a reader can run Part A alone to inspect the model without solving.
- Flat: no classes, no hidden state. Each cell is a self-contained step with visible inputs and outputs.
- Mirrors the math: cell comments reference the semiconjugate formula \(\mathbf{S}^n = \mathbf{E} \circ \hat{\mathbf{S}}^{n-1} \circ \mathbf{M}\) so the reader can map each code step to the corresponding operator.
- Parameterised: \(H\) and
a_gridare set in one cell (Part A, step 3) and consumed downstream. Changing \(H\) and re-running Part B produces a different horizon solution.
5.3 Existing notebook: egm_cons_stage_pipeline.ipynb¶
Remains as-is. It demonstrates the dolo-plus four-step pipeline (parse → methodize → calibrate → configure) on the split-file EGM example. It does not solve — it produces a configured SymbolicModel ready for translation.
The new semiconjugate_solve.ipynb picks up where this notebook leaves off: it takes a translated (or native) horse and solves the finite-horizon problem.
6. Success criteria¶
0.1eA is successful when:
- A single vanilla Dolo horse (translated or native) can be fed to
egm()with finite-horizonmaxitanddr0. - The EGM solver produces \(m\)-space policies \(\hat{\mathbf{S}}^{H-1} \circ \mathbf{M}\, w_H\) without modifications.
- A final \(\mathbf{E}\) step using compiled gufunctions recovers the \(a\)-space marginal return
mr(a). - Policy plots match expected finite-horizon consumption behaviour (concave, increasing in \(m\), converging as \(H\) grows).
7. Implementation agnosticism¶
The Bellman calculus representation is agnostic to how the sub-operators are executed:
- A Dolo horse can swallow an entire period and compute \(\hat{\mathbf{S}}\) as a single compiled step (this spec).
- A stage-separated orchestrator can call expectation and maximisation as separate primitives (Appendix A).
- A generic operator can replace \(\mathbf{M}\) with policy iteration, marginal value iteration, or any other method sharing the same signature.
The mathematical content — the stages, their transitions, and the semiconjugate relationship — is invariant across all three approaches.
References¶
- Solving via Conjugation
- semiconjugacy-iterates
- Spec 0.1b: dolo-plus → Dolo translator
- Stachurski, J. (2009). Economic Dynamics. MIT Press.
- Stachurski, J. (2022). Economic Dynamics, 2nd ed. MIT Press.
- Sargent, T. J. and Stachurski, J. (2025). Dynamic Programming, vol. 2. Manuscript.
Appendix A: Stage-separated approach (alternative)¶
The stage-separated approach uses operator models — vanilla Dolo models that compile a subset of dtcc blocks for use by an external orchestrator. Each stage is translated independently:
- noport operator model: compiles
half_transitiononly (expectation primitive) - cons operator model: compiles
direct_response_egm+reverse_state(maximisation primitives)
The orchestrator (explore/transformations/egm-stage-operators-to-dolo/backward_step_noport_cons.py) implements the backward step by calling the gufunctions manually.
This approach offers stage-local numerical freedom and faithful staging, but duplicates logic already inside egm(). The monolithic horse (§2–§3) is preferred for the noport → cons case because:
- It reuses Dolo's existing convergence checks, decision-rule interpolation, and grid management.
- No custom orchestrator code is needed.
- The semiconjugate transform guarantees mathematical equivalence.
The operator-model approach may be revisited for mixed-method periods (e.g. portfolio stages) where egm() cannot handle the full period monolithically.
A.1 Operator-model artifacts¶
| File | Role |
|---|---|
explore/transformations/egm-stage-operators-to-dolo/noport_operator/mapping_tables.yaml |
noport operator mapping |
explore/transformations/egm-stage-operators-to-dolo/cons_operator/mapping_tables.yaml |
cons operator mapping |
explore/transformations/egm-stage-operators-to-dolo/backward_step_noport_cons.py |
Stage-separated orchestrator |
explore/transformations/egm-stage-operators-to-dolo/translate_stage_to_operator.py |
Per-stage translator |