Skip to content

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:

  1. translating a materialized dolo-plus stage into a vanilla Dolo horse via doloplus_to_dolo,
  2. feeding the horse directly to Dolo's egm() solver, and
  3. 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:

\[ \mathbf{E}\colon v \mapsto w, \quad (\mathbf{E}\,v)(a) = \mathbb{E}_\theta[v(aR + \theta)], $$ $$ \mathbf{M}\colon w \mapsto V, \quad (\mathbf{M}\,w)(m) = \max_{c \in (0,m)}\{u(c) + \beta\, w(m - c)\}. \]

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:

\[ \mathbf{S}^n = \mathbf{E} \circ (\mathbf{M} \circ \mathbf{E})^{n-1} \circ \mathbf{M} = \mathbf{E} \circ \hat{\mathbf{S}}^{\,n-1} \circ \mathbf{M}. \]

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
 = 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 inside direct_response_egm:. The mapping tables in this repo currently place \(\beta\) in direct_response_egm: (coming from InvEuler) and leave it out of expectation:.

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\):

\[ \mathbf{S}^n \, w_H = \mathbf{E} \circ \hat{\mathbf{S}}^{\,n-1} \circ \mathbf{M} \, 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:

model = yaml_import(
    "packages/dolo/examples/models/consumption_savings_iid_egm.yaml"
)

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:

dr0 = lambda i, s: s   # c = m (consume all cash-on-hand)

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 (includes InvEuler, 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_transition
  • auxiliary_direct_egm
  • reverse_state
  • expectation
  • direct_response_egm
  • arbitrage

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:

  1. A dolo_plus header: {dialect: adc-stage, version: 0.1}
  2. A total equation_symbols mapping (label → canonical symbol)
  3. 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 values
  • domain: and options: for grid configuration
  • exogenous: 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

  1. Setup + imports. Add repo packages to path; import yaml_import, egm, numpy, matplotlib.

  2. Load the horse. Either:

  3. (a) translate from dolo-plus via doloplus_to_dolo (shows the full pipeline), or
  4. (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.

  1. Set parameters. Horizon \(H\), asset grid a_grid, terminal condition dr0 = lambda i, s: s.

Part B — Solve

  1. Run egm(). Call egm(model, dr0=dr0, a_grid=a_grid, maxit=H-1, η_tol=0.0, verbose=True). Print iteration progress. This produces sol.dr — the \(m\)-space policies after \(\hat{\mathbf{S}}^{H-1} \circ \mathbf{M}\, w_H\).

  2. Final \(\mathbf{E}\) step. Extract compiled gufunctions (half_transition, expectation) and run the expectation-accumulation loop once to recover mr(a) on the asset grid.

Part C — Inspect

  1. 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.

  2. 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_grid are 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:

  1. A single vanilla Dolo horse (translated or native) can be fed to egm() with finite-horizon maxit and dr0.
  2. The EGM solver produces \(m\)-space policies \(\hat{\mathbf{S}}^{H-1} \circ \mathbf{M}\, w_H\) without modifications.
  3. A final \(\mathbf{E}\) step using compiled gufunctions recovers the \(a\)-space marginal return mr(a).
  4. 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


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_transition only (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