Skip to content

Related

Baseline (sequential-only): Spec 0.1h — Periods and Nests

Foundations: Periods & Nests Theory, Core DDSL Concepts

Semantic Rules: 05 Periods & Models, 03 Equations

Prior art (Dyn-X): AI/context/external/ModularMDP-repos/Dyn-X/ — housing tenure choice implementation

Conceptual draft: AI/working/AAS/29012026/branching-explore.md.md

Spec 0.1l — Branching stages

Overview

This specification extends the sequential composition framework of spec 0.1h to support branching: compositions where a stage routes to multiple successor stages or periods, rather than a single one. Branching arises naturally when the continuation state is a coproduct indexed by a finite label set and the value functions along each branch are named and separable — that is, each branch has its own named continuation perch with a distinct state space and value function.

Branching is the coproduct (sum) in the composition algebra: where sequential composition chains a single successor, branching fans out to several.

In our initial spec, the branches are aggregated in the backward cntn to dcsn move by one of two operators:

  • max (discrete choice): \(\mathrm{v} = \max_j \mathrm{v}_{\succ,j}\) — the agent chooses the best branch
  • expectation (stochastic): \(\mathrm{v} = \sum_j p_j \mathrm{v}_{\succ,j}\) — weighted average over branches

The expectation operator subsumes both exogenous and endogenous probability schemes:

  • Exogenous probabilities: declared in parameters: or exogenous: (e.g., survival probability)
  • Endogenous probabilities: computed from the branch value functions themselves via AGGREGATE (methodized to softmax/logit/etc.)

In the endogenous case, the probabilities are generated first from the branch values (via the AGGREGATE operator), then used in a standard weighted sum. The specific kernel (softmax, probit, etc.) is a methodization choice — no additional surface-syntax "mode" is needed.

Remark: Both max and expectation can be unified as the choice over a probability density. The max operator chooses the degenerate measure on the best branch; expectation evaluates a given measure. When that measure is itself derived from the values (endogenous AGGREGATE), the probability generation step precedes the expectation — the aggregation mode remains expectation.

#ambiguity

This remark (unification of max and expectation as measures) is unclear — revisit.

Key design principle: branching is a stage-level concept, not a connector concept. A branching stage declares multiple named continuation perches, and its decision mover combines multiple continuation values in the backward Bellman equation (either by max_d{…} or by an explicit weighted sum). Composition with successor stages uses the existing namespace rule: continuation perch fields match successor arrival fields by name (optionally after an intra-period connector), and inter-period wiring uses standard inter-period connectors. No special "branching connector" syntax is introduced.

Namespace discipline: no overloading, underloading OK. Within a period namespace, each variable name refers to exactly one quantity — two stages may not output different things under the same name. A stage may underload (consume a subset of the namespace). When a generic stage template needs to be reused in two branches, an intra-period connector (rename map) adapts its fields so that each occurrence's names are unique in the namespace.

Identity-transition caveat. When the transition between two perches is the identity map (e.g. arvl_to_dcsn_transition: a = a[<]), the variable name may be retained across both perches — this is not overloading, because the quantity is the same. A fresh name is required only when a transformation produces a genuinely new quantity (e.g. w = (1+r)*a[<] + y).

#ambiguity

How does the construction of different information sets across the branches square up with the information set view of perches? Does each branch cntn perch contain its own information set?

1. Motivation and scope

1.1 When branching arises

Branching is required when:

  1. The continuation is indexed by a finite label set (a coproduct label) that distinguishes qualitatively different syntactic successor stages (e.g., own vs. rent, employed vs. retired, alive vs. dead).
  2. Value functions are separable across the discrete index: each regime has its own named value function and different continuous state space.
  3. The decision or nature selects which regime applies: either the agent chooses (discrete choice: max) or nature draws (stochastic: weighted sum / expectation).

A within-stage discrete choice over a finite set (e.g., choosing \(H' \in \mathcal{H}\) where all choices share the same state space structure) where each choice leads to the same syntactic stage does not require branching — it is handled by the existing max_c{...} operator within a single stage's mover (see housing_choice_mdp.md). Branching is for syntactically heterogeneous successors: different state spaces, different value functions, different downstream stage problems.

1.2 Relationship to spec 0.1h

Spec 0.1h establishes:

  • Periods as namespaces with ordered stage lists
  • Nests as sequences of period instances wired by positional twisters
  • Connectors: intra-period (connectors) and inter-period (connectors) as rename morphisms
  • All of the above restricted to sequential (linear chain) composition

This spec generalizes the composition graph from a path to a directed acyclic graph (DAG) by introducing:

  • Branching stages: stages with multiple named continuation perches and a backward cntn to dcsn mover equation that combines multiple branch values
  • DAG periods: stage lists that admit fan-out (and eventual fan-in at period/twister boundaries)

Composition between a branching stage's continuation perches and successor stages uses the same namespace rule as sequential composition: wiring is induced by the shared namespace (after any connector renames are applied), with the additional constraint that variable names may not be overloaded (each name refers to one quantity; underloading is OK).

1.3 Scope boundaries

In scope (this spec) Out of scope (deferred)
Branching stages (multiple continuation perches) Nested branching (branch targets that themselves branch) — permitted but solver support deferred
max and expectation (weighted sums); endogenous probabilities via AGGREGATE Custom aggregation operators; non-Gumbel taste-shock kernels
Deterministic and probabilistic branching Continuous-valued branch selection
DAG composition within periods Cyclic graphs (infinite-horizon branching)
Population dynamics with branch-specific measures Distributional branching beyond measure splitting
DAG nests with explicit twister targets General graph nests (merging from multiple predecessors)

#ambiguity

We are not assigning weights to each branch which determine the measure of agents at that branch (the weight is relevant only at the aggregation point); however, in forward simulation, we assume that the mass of agents is appropriately split into the branched fields — so \(x_{\succ, d}\) for a particular random variable may have a distribution that does not sum to one, for instance.

2. Theory (reference)

This dev-spec is split into two documents:

  • Theory/reference (coproduct continuation spaces, multi-source movers, endogenous probabilities via AGGREGATE, index-shape/pushout wiring intuition): spec_0.1l-branching-theory.md
  • Syntax & implementation (YAML shape, runtime representation, builder algorithm, validation): this document

See: Spec 0.1l — Branching stages (theory).

3. YAML surface syntax

3.1 Branching stage declaration

A branching stage declares multiple continuation perches (branches) in its poststates block.

Design goal: keep the surface syntax as close as possible to the sequential case. Concretely:

  • poststates becomes branch-keyed using {label}: sub-blocks (e.g. own:, rent:) when kind: branching.
  • dcsn_to_cntn_transition becomes branch-keyed using {label}: | sub-blocks (e.g. own: |, rent: |) when kind: branching.
  • The backward mover (cntn_to_dcsn_mover) stays an ordinary mover block: branching is expressed by equations that reference multiple branch-continuation values (e.g. V[>][own], V[>][rent], desugaring to V_own_func[>], V_rent_func[>]) and combine them with max_d{own,rent} or a weighted sum.
  • If the branch specific cntn value function is named, a valid alternative is V_own[>] so V_own[>] =V[>][own], but the name V_own is not implicit, it is specified by declaration that V_own is the name of the own branch VF.
stage:
  name: "TenureChoice"

symbols:
  spaces:
    Xa: "@def R+"
    XH: "@def linspace(H_min, H_max, n_H)"
    XY: "@def {1, ..., n_y}"
  prestate:
    b: "@in Xa"
    H: "@in XH"
    y_pre: "@in XY"
  states:
    a: "@in Xa"
    H: "@in XH"
    y: "@in XY"
  poststates:
    # Branch-specific continuation states (`{label}:` sub-blocks).
    # Each branch uses unique field names in the period namespace.
    own:
      w_own: "@in Xa"       # owner cash-on-hand
      H: "@in XH"
      y_own: "@in XY"
    rent:
      w_rent: "@in Xa"      # renter cash-on-hand after liquidating housing
      y_rent: "@in XY"
  controls:
    d: "@in {own, rent}"     # discrete branch selector (tenure choice)
  exogenous:
    y:
      - "@in XY"
      - "@dist MarkovChain(Pi, z_vals)"
  values:
    V[<]: "@in R"
    V: "@in R"

    # Branch-keyed continuation value family (label → value symbol name).
    # Keys must match `poststates.{label}` and `dcsn_to_cntn_transition.{label}`.
    V[>]:
      own: "@in R"
      rent: "@in R"

    V_own: "@in R"           # continuation value (own branch)
    V_rent: "@in R"          # continuation value (rent branch)

  parameters: [r, phi]

Provisional declaration shape (v0.1l)

The label-keyed branching declarations used here — poststates.{label}, the values.V[>] branch map (keys {label} mapping to value symbols V_{label}), and equations.dcsn_to_cntn_transition.{label} — are provisional and may be revised once we lock the canonical SYM IR for branching/value families.

Semantically, the continuation state is a coproduct (disjoint union) of branch-specific spaces; see the companion theory note for the precise construction.

In the owner/renter example, the branch label is not a state variable

In the tenure-choice fan-out, the two continuation branches have different field schemas:

  • own continues with (a, H, y_own)
  • rent continues with (w, y_rent)

That schema difference already determines which summand you are in (and which successor-stage interface you can wire into), so you do not need to carry an explicit "own/rent" tag as an additional declared state field. The label lives at the wiring/type level (which poststates.{label} block you are in), not as an economic variable.

This will have implications for how we carry distributions forward. #ambiguity #distributions #simulation

#ambiguity

It seems a bit awkward to carry around the y_own and y_rent tag — revisit naming convention.

Key structural rules:

  1. poststates contains {label}: sub-blocks, one per branch. Each sub-block is a distinct continuation perch with its own state space.
  2. Branch labels (e.g. own, rent) are reused consistently:
  3. as keys under poststates (own:, rent:),
  4. as keys under equations.dcsn_to_cntn_transition (own:, rent:, see §3.2),
  5. as keys under values.V[>] (own: V_own, rent: V_rent).
  6. The branch-label set must be consistent across the branch-keyed sub-blocks:
  7. poststates.{label},
  8. the keys of values.V[>] (and, for each label, the mapped symbol values.V[>].{label} must be declared under values:), and
  9. equations.dcsn_to_cntn_transition.{label} (and any max_d{…} / argmax_d{…} over branches).
  10. Branch-specific continuation values are declared as a branch-keyed family under values.V[>] (e.g. own: V_own, rent: V_rent). The mapped symbols (V_own, V_rent) are ordinary value objects (typed in values:) and are referenced in equations either explicitly as V_own[>], V_rent[>] or using the canonical sugar V[>][own], V[>][rent] (see below).

Value-family selection sugar (branching): in a branching stage, treat V[>][label] as sugar for V_{label}[>], where V_{label} is looked up from the declared map values.V[>].{label}. 5. For discrete choice, the decision mover uses a max_d{…} over the branch values (and one typically recovers an argmax branch indicator as a policy). For probabilistic branching, the decision mover uses an explicit weighted sum \(\sum_j p(j)\,\mathrm{v}_{\succ,j}\) (where \(p\) is exogenous or computed endogenously).

3.2 Branching transitions

Each branch has its own decision→continuation transition, declared as a label-keyed sub-equation under dcsn_to_cntn_transition.

Important: We are now thinking of this as branching or a co-product occurring between the decision and continuation perch.

equations:
  arvl_to_dcsn_transition: |
    a = (1 + r)*b[<] + z_vals[y]
    H = H[<]
    y = y_shock

  dcsn_to_cntn_transition:
    own: |
      w_own[>] = a + H
      H[>] = H
      y_own[>] = y

    rent: |
      w_rent[>] = (1+r)*a + H
      y_rent[>] = y

3.3 Branching movers

The backward mover at a branching stage is still “just equations”: it combines multiple branch continuation values, and the combining operator is written directly in the Bellman equation (e.g. max_d{…} or a weighted sum).

equations:
  # Backward mover: multiple continuation → single decision
  cntn_to_dcsn_mover:
    Bellman: |
      V = max_d{V[>][own], V[>][rent]}

  # Standard backward mover (non-branching)
  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = E_y(V)

For endogenous probabilities (taste shocks), see §3.7. The key idea is: compute \(p\) inside the mover equations (e.g. p = AGGREGATE(...), where AGGREGATE is methodized to softmax/logit/etc.) and then use \(p\) in an explicit weighted sum.

3.4 Period template with branching (no overloading; connectors for renames)

Wiring within a branching period follows the same namespace rule as sequential composition, with one additional constraint:

No overloading; underloading OK

Within a period namespace, each variable name refers to exactly one quantity: each node is named by its field. Two stages may not contain different states under the same name (no overloading) unless the states are isomorphic (joined by an edge); and in this case they must follow the edge composition rules (dcsn→arvl etc). A stage may consume only a subset of the namespace (underloading is OK) and connect to another stage via an intra-period connector.

Identity-transition exception. When a perch-to-perch transition is the identity (no transformation), the same variable name is retained at both perches. For example, a branching stage with a = a[<] (identity arrival) legitimately uses a at both arrival and decision. A new name is required only when a transformation produces a new quantity.

Fan-in vs fan-out constraint on connectors

If the successor stage for each branch is unique (e.g. own and rent go to separate stages), there is no namespace collision and no connector is needed. A connector (rename map) is required only when a generic stage template is reused across branches and its field names would collide in the shared namespace. Connectors are period-wide objects — they live in the period template, not in the stage. They are rename maps applied at a specific stage occurrence in the period's DAG, transforming that occurrence's field names so they can form valid edges in the namespace.

In the housing example below, w_own and w_rent could fan in to a shared w (if both branches used the same successor stage, in the next period, it will be the same tenure stage), but w cannot be the continuation state of two different stages fanning out — that would violate the no-overloading rule. In the current housing example, owner_cons and renter_cons are separate stages, so the connectors below are fan-in renames (not fan-out). #fan-in #no-overloading

name: housing_tenure_period

stages:
  - tenure_choice: !stage    # branching stage. cntn branches w_own, w_rent
  - owner_housing: !stage    # own path gets w_own as arvl
  - owner_cons: !stage       # own-path consumption gets m_own. a_nxt and H_nxt are the cntn states. 
  - renter_housing: !stage   # rent path gets w_rent as arvl
  - renter_cons: !stage      # rent-path consumption gets m_rent as arvl. a_nxt and H_nxt are the cntn states. 

# no intra-period connectors since name space uniquely defines branching

Important

In the above setup, renter_cons and owner_cons both return a_nxt and H_nxt. This implies fan-in (in the forward graph) into the next period's tenure choice. Fan-in implies aggregation of population measures in the forward graph.

Connectors are period-wide objects, not stage-to-stage edges

Spec 0.1h defined connectors as edges {from, to, rename} between stage pairs. This spec redefines them as connectors — rename maps applied at a stage occurrence, not between two stages. They are simpler (no need to reference pairs of stages) and align with the principle that wiring is namespace-driven: the connector adjusts a node's field names, and composition follows from name matching.

Fan-out and fan-in (forward graph). These terms are always defined with respect to the forward (population/simulation) graph:

  • Fan-out: one continuation perch has outgoing edges to multiple arrival perches. Within a branching stage, the decision perch fans out to multiple continuation sub-spaces — but these are internal to the stage. Between stages/periods, the single continuation field set maps to multiple successors (e.g. a retirement-choice stage branches into worker and retiree paths).
  • Fan-in: multiple continuation perches have incoming edges into a single arrival perch. Once paths merge, the agent cannot distinguish which path was taken — this is the lattice-recombination condition (e.g. both worker and retiree paths converge into a common terminal stage).

Structural rules

Fan-out occurs at the dcsn→cntn boundary (within a branching stage). Fan-in occurs at the arrival perch (multiple predecessor continuation fields map to the same arrival state). Fan-in requires that all merging connector functions have the same codomain (arrival state space) — the arrival value function depends on the arrival state only, not on the identity of the source branch.

Forward–backward duality:

Forward (population dynamics) Backward (Bellman equation)
Fan-out: one state → multiple successor stages Multiple successor values → one current value (max / E / log-sum-exp)
Fan-in: multiple predecessor stages → one state One continuation value feeds multiple predecessor stages

#ambiguity

A rule may be that fan-in is only allowed at the arrival perch and that fan-out is only allowed at the dcsn→cntn move. Revisit.

Measures + join semantics (theory)

For the formal measure-theoretic treatment of (i) branch splitting as pushforwards, (ii) fan-in/joins into a common successor, and (iii) the “same successor value function” pullback viewpoint, see Spec 0.1l — Branching stages (theory) §1.4.

3.5 Nest template with inter-period connectors

Consider a retirement choice model (Iskhakov et al. 2017) where the choice of whether to work or retire in \(t{+}1\) is made in period \(t\). Each time slot is one period containing the branching stage and both branch paths (worker and retiree consumption). The branching is internal to each period — the nest remains a linear chain, not a DAG.

Key principle: branching is a stage-level concept. Each period contains a branching stage (retirement choice) plus both branch successor stages (worker consumption, retiree consumption). The nest lists one period per time slot with plain inter-period connectors between adjacent periods.

name: retirement_lifecycle

periods:
  - working_period: !period    # t=0: branching + both branches inside
  - working_period: !period    # t=1
  - working_period: !period    # t=2
  - terminal: !period          # t=3: terminal value

connectors:      # inter-period (backward convention: keys=later, values=earlier)
  - {a_nxt: b}
  - {a_nxt: b}
  - {a_nxt: b}

The working_period template contains the branching stage plus both worker and retiree successor stages (as in the housing example, §3.4). Both branches produce a_nxt as their continuation field, which the inter-period connector renames to b for the next period's arrival. The backward mover at the branching stage combines the branch values via max (discrete choice).

No special branching connector

The connectors above are standard rename maps (same as spec 0.1h). The branching logic (max vs weighted sum) and any probability computations live in the stage's backward mover equations — the connector knows nothing about branching.

Intra-period terminal branches

When a branch has a terminal resolution (e.g., death → bequest function) that can be evaluated locally within the period, handle it as an intra-period terminal stage rather than routing to a shared terminal period. This keeps the nest sequential and avoids path proliferation. See §6.2 (lifecycle mortality) for the canonical pattern.

3.6 Aggregation semantics

The combining operator is written directly in the stage's backward mover equations, not in connectors:

Aggregation Backward (value functions) Forward (simulation) Probabilities
max \(\mathrm{v}(x) = \max_{j \in N_+} \mathrm{v}_{\succ,j}\!\bigl(\mathrm{g}_{\sim\succ,j}(x)\bigr)\) Route to \(j^* = \arg\max_j\); population partitions Degenerate: \(p = \mathbf{1}\{j = j^*\}\)
expectation \(\mathrm{v}(x) = \sum_{j \in N_+} p_j(x) \, \mathrm{v}_{\succ,j}\!\bigl(\mathrm{g}_{\sim\succ,j}(x)\bigr)\) Draw \(j \sim p(\cdot \mid x)\); population splits by weight Exogenous or endogenous (see below)

Probability sources for expectation:

Source Declaration Example
Exogenous (fixed) parameters: Survival probability surv_prob
Exogenous (stochastic) exogenous: Shock-dependent transition probability
Endogenous (value-derived) Compute inside cntn_to_dcsn_mover equations + parameters: for scale p = AGGREGATE(V[>][own], V[>][rent]; sigma). Methodization: cntn_to_dcsn_mover.AGGREGATE: softmax{settings:...}

For endogenous probabilities, the probability generation is an explicit computational step written in the mover equations (typically before the weighted sum that defines V). See §3.7 for the canonical pattern.

AGGREGATE requires a new operation type #todo/syntax #aggregation

The AGGREGATE operator is a new SYM-level construct: it takes branch values and returns a probability vector. Its specific form (softmax, probit, etc.) is a methodization choice. This implies a new entry in the operation/scheme registry.

3.7 Endogenous branch probabilities — syntax

taste-shock #endogenous-probability #softmax

When branch probabilities depend on the value function (e.g., taste-shock models), the approach is: generate probabilities first, then use standard expectation. This avoids introducing a new aggregation mode. The two-step pattern is:

  1. Compute \(p(j)\) from the branch value functions (inside the mover equations)
  2. Use the computed \(p(j)\) as weights in a standard weighted sum

The general branching equation (adapted from Dyn-X, in conventions notation):

\[ \mathrm{v}(x) = \max_a \left\{ \mathrm{r}(x, a) + \beta(x) \sum_{j \in N_+} p(j \mid x, a) \, \mathrm{v}_{\succ,j}\!\bigl(\mathrm{g}_{\sim\succ,j}(x, a)\bigr) \right\} \]

The probabilities \(p(j \mid x, a)\) can be exogenous or endogenous. In both cases, the aggregation is expectation — the only difference is how \(p\) is determined:

Probability source How \(p\) is determined YAML declaration
Exogenous Declared directly in parameters: or exogenous: Standard — no new syntax
Endogenous (softmax) \(p(j) = \exp(\mathrm{v}_{\succ,j} / \sigma) / \sum_k \exp(\mathrm{v}_{\succ,k} / \sigma)\) Compute in cntn_to_dcsn_mover equations
Endogenous (general) Any function of branch values Compute in cntn_to_dcsn_mover equations

Proposed syntax — explicit probability generation:

equations:
  cntn_to_dcsn_mover:
    Bellman: |
      p = AGGREGATE(V_pi1[>], V_pi2[>], V_pi3[>]; sigma_pi)
      V = p(1)*V_pi1[>] + p(2)*V_pi2[>] + p(3)*V_pi3[>]

The specific kernel (softmax, probit, etc.) is specified in methodization: cntn_to_dcsn_mover.AGGREGATE: softmax{sigma: sigma_pi}.

Key design properties:

  • No new mover metadata keys. The branching logic is visible in the equations: max_d{…} for discrete choice, and an explicit weighted sum for probabilistic branching.
  • AGGREGATE is the SYM-level operator for endogenous probability generation. It maps a vector of branch values (and optional parameters) to a probability vector. The specific kernel is a methodization choice: when methodized as softmax, AGGREGATE(v_1, ..., v_K; σ) = exp(v_k/σ) / Σ exp(v_l/σ). #aggregation #discrete-choice
  • p is an ordinary declared variable (recommended: in values:) computed in the mover equations and then used as weights. The solver knows these weights are endogenous and must be recomputed each iteration.
  • Euler equation compatibility: The envelope condition gives \(\partial \mathrm{v} / \partial x = \sum_j p(j) \, \partial \mathrm{v}_{\succ,j} / \partial x\) — the same functional form as exogenous expectation, but with endogenous \(p\). This means existing EGM solvers can handle it by using the probability-weighted marginal values.

Open syntax questions #todo/syntax #endogenous-probability

  1. Type of p: We declare p as a typed field (recommended: in values:). What is the canonical domain syntax for probability vectors — @in simplex(K) or something else?

  2. General probability functions: The syntax supports any function written in the mover equations, not just softmax. For example, p = normalize(f(V_1, ..., V_K)) for an arbitrary mapping. Should the spec constrain this to known closed-form kernels (softmax, probit, etc.) or allow arbitrary expressions?

  3. Relationship to the Dyn-X smooth_sigma: In the Dyn-X codebase, smooth_sigma is a solver parameter (Ρ-level), suggesting the smoothing is a numerical concern rather than a model primitive. If the modeller views taste shocks as economically meaningful, the probability-generation equations belong in the SYM layer. If taste shocks are a computational device, the smoothing could instead be a methodization annotation. This is an Υ/Ρ boundary question. #Υ-Ρ-boundary

3.8 Branch control attribute

Each fan-out connector (or the branching stage itself) carries a branch_control attribute that determines the backward aggregation operator:

Branch control Backward aggregation Example
agent Upper envelope: \(v(x) = \max_j v_j(x)\) Retirement choice (DC-EGM)
nature Expectation: \(v(x) = \sum_j p_j \, v_j(x)\) Scenario branching, survival probability
mixed Log-sum-exp: \(v(x) = \sigma \log \sum_j \exp(v_j(x)/\sigma)\) Additive taste shocks (Rust 1987, logit smoothing)

Non-anticipativity property. Because fan-out occurs at or after the continuation perch, all controls chosen at the decision perch are branch-invariant. This is guaranteed by the perch ordering (arrival → decision → continuation). This aligns with Rockafellar & Wets (1991) and non-anticipativity constraints in stochastic programming.

Fan-in measurability. All connector functions feeding a fan-in arrival perch must have the same codomain (arrival state space). The arrival value function depends on the arrival state only — not on the identity of the source branch. This is the lattice-recombination condition.

3.9 Action-dependent branching

Two fundamentally different branching patterns exist:

Pattern Structure Solver
Pure discrete branching Agent picks branch \(j\) directly, then optimizes continuous \(\pi_j\) within branch \(j\). Separable — upper envelope applies. DC-EGM works
Action-dependent branching Branch probabilities \(p(j \mid x, \pi)\) are smooth in continuous action \(\pi\). Non-separable — requires joint optimization over \(\pi\). DC-EGM does not apply; single joint optimization

For action-dependent branching: \(v(x) = \max_\pi \bigl\{ r(x, \pi) + \beta \sum_j p(j \mid x, \pi) \, v_j(g_j(x, \pi)) \bigr\}\). Note: logit/taste-shock branching (\(\sigma \log\sum\exp\)) is structurally different — it arises from integrating out additive EV1 taste shocks and the implied choice probabilities depend on value indexes, not directly on continuous controls. This is the "mixed" case, not the "action-dependent" case.

#ambiguity

Do we need to support action-dependent branching in spec 0.1l, or is pure discrete branching sufficient for now?

#ambiguity #branching/timing

Uncertainty timing — post-decision vs recourse. The non-anticipativity property (§3.8) assumes post-decision fan-out: branching at or after the continuation perch, so controls are branch-invariant. Standard multistage stochastic programming also allows recourse: branching before the next decision, with controls adapted to the realized branch. The ADC perch structure has a natural slot for this — the arrival-to-decision transition can be the point where nature branches. Resolve before finalising: define which perch boundaries allow fan-out and state the non-anticipativity condition for each case.

#ambiguity

SP interoperability (scenario-tree interchange format) is deferred — namespace-driven wiring is equivalent to an explicit graph for Bellman recursion, but not sufficient for SP algorithms that consume explicit tree objects with arc probabilities and nonanticipativity sets. Revisit if SP interop becomes a goal.

4. Runtime representation shapes

4.1 Connectors (period-wide namespace renames)

A connector is a rename dict {a: k} (YAML) / {"a": "k"} (Python). Intra-period connectors are associated with stage occurrences; inter-period connectors sit between adjacent periods. Both are period-level objects, not embedded in stage templates.

Stage occurrences that need no renaming simply omit a connector (identity wiring).

4.2 Inter-period connectors

Inter-period connectors follow the same rename-dict convention. Directional convention: keys name variables in the backward-preceding period (later in forward time), values name variables in the backward-successive period (earlier in forward time).

4.3 Period data structure

A period is a plain dict: stages + intra-period connectors (matching the notebook, md-cells/10-build-periods.md):

period = {
    "stages":     {"tenure_choice": ..., "owner_cons": ..., "renter_cons": ...},
    "connectors": [{"w": "w_own"}, {"w": "w_rent"}],   # intra-period connectors (rename dicts)
}

4.4 Nest data structure

A nest is a plain dict: periods + inter-period connectors:

nest = {
    "periods":          [period_H, period_H1, ..., period_0],
    "inter_connectors": [{"a_nxt": "b"}, {"a_nxt": "b"}, ...],
}

4.5 Solution storage at branching stages

When a stage has multiple continuation perches, the solution at the continuation is a dictionary keyed by branch label:

# Solution at a branching stage's continuation
stage.cntn_sol = {
    "own": Solution(policy=..., value=...),   # own-path solution
    "rent": Solution(policy=..., value=...),  # rent-path solution
}

# The combined decision-perch value is the aggregated result
stage.dcsn_sol = Solution(
    value=...,           # max or weighted sum of branch values
    policy=...,          # includes branch selector (d*) and within-branch policies
)

5. Builder algorithm: backward induction with branching

At this layer, branching is detected in the stage object, via label-keyed continuation perches under poststates.{label} and label-keyed transition sub-equations under dcsn_to_cntn_transition.{label}. Period and nest interfaces (connectors/twisters) remain wiring-only rename maps; they do not encode aggregation logic.

5.1 Backward solve order within a branching period

The backward solve must respect the DAG structure. For the housing example:

Solve order (backward):
  1. owner_cons     (own-path consumption stage)
  2. renter_cons    (rent-path consumption stage)
  3. owner_housing  (own-path housing stage)
  4. renter_housing (rent-path housing stage)
  5. tenure_choice  (BRANCHING: max{V[>][own], V[>][rent]} + E_y[...])

At step 5, the backward mover at tenure_choice receives: - V[>][own] = arrival value from owner_housing (via namespace / connector) - V[>][rent] = arrival value from renter_housing (via namespace / connector)

and combines them via max (discrete choice).

General rule: in a branching period, the solve order is a topological sort of the stage DAG (reversed). All stages downstream of each branch must be solved before the branching stage itself.

5.2 Backward accretion with inter-period connectors

When building a nest backward from terminal, each inter-period connector (a rename dict) determines the variable mapping between adjacent periods:

# Pseudocode: backward accretion
nest = {"periods": [], "inter_connectors": []}

# Step 1: terminal period
terminal_period = make_period(terminal_template, terminal_calibration)
solve_period(terminal_period, terminal_value=V_terminal)
nest["periods"].append(terminal_period)

# Step 2: accrete backwards
for h in range(1, H+1):
    period = make_period(template, calibration_at_h)

    # Apply the inter-period connector (rename dict) to map
    # continuation fields from the already-solved period
    # into arrival fields of the current period.
    connector = inter_connectors[h - 1]
    arrival_value = get_arrival_value(nest["periods"][-1])
    renamed_value = apply_rename(arrival_value, connector)

    # The branching stage's backward mover combines the continuation values.
    # The combining logic (max vs weighted sum) lives in the mover equations.
    solve_period(period, continuation_values=renamed_value)

    nest["periods"].append(period)
    nest["inter_connectors"].append(connector)

5.3 Forward simulation with branching (not part of the builder)

Forward simulation is not part of the period/nest builder. The builder’s responsibility is to assemble stage/period/nest instances, apply connectors, and pass continuation objects into backward movers.

For execution/simulation, the only contract the branching stage needs to expose is:

  • a deterministic branch selector (e.g. an argmax policy like d*) or
  • branch probabilities p(·) (exogenous or computed endogenously via AGGREGATE),

plus the branch-specific forward transitions dcsn_to_cntn_transition.{label}.

Population routing / mass-splitting / measure addition belongs to the solver/simulator layer (see the companion theory note spec_0.1l-branching-theory.md §1.4 for measure semantics).

6. Canonical examples

6.1 Housing tenure choice (intra-period branching, max)

This is the canonical example from Dyn-X. A household chooses between owning and renting housing.

Period structure:

TENU (branching, max)
  ├── own  → OWNH → OWNC ──┐
  └── rent → RNTH → RNTC ──┘──→ [next period TENU]

Stage: TenureChoice (branching)

  • Arrival: \((b, H, y_\text{pre})\) — assets, housing stock, pre-shock income
  • Decision: \((a, H, y)\) — after income shock realization; \(a = (1+r)b + z[y]\)
  • Continuation (own): \((w_\text{own}, H, y)\) — owner cash-on-hand: \(w_\text{own} = a + H\)
  • Continuation (rent): \((w_\text{rent}, y)\) — liquidates housing: \(w_\text{rent} = (1+r)a + H\)

Backward mover:

\[ \mathrm{v}(a, H, y) = \max\bigl\{ \mathrm{v}_{\succ,\text{own}}(w_\text{own}, H, y), \; \mathrm{v}_{\succ,\text{rent}}(w_\text{rent}, y) \bigr\} \]

State spaces and morphisms:

                                    ┌─own──▶ (w_own, H, y) ──H'──▶ (w_oc, H_nxt, y) ──c──▶ (a_nxt, H_nxt, y)
(b, H, y_pre) ──shock y──▶ (a, H, y)┤
                                    └─rent─▶ (w_rent, y) ────h'──▶ (w_rc, h, y) ──────c──▶ (a_nxt, y)

All intermediate state spaces have unique names (w_own, w_oc, w_rent, w_rc). No connectors needed.

Period template:

name: housing_tenure_period

stages:
  - tenure_choice: !stage    # branching
  - owner_housing: !stage    # own path
  - owner_cons: !stage       # own-path consumption
  - renter_housing: !stage   # rent path
  - renter_cons: !stage      # rent-path consumption

# No connectors needed — all names are unique by construction.

Wiring is implicit: tenure_choice.own fields (w_own, H, y) match owner_housing.arvl; tenure_choice.rent fields (w_rent, y) match renter_housing.arvl. Likewise within each branch.

6.2 Lifecycle mortality (intra-period branching, expectation)

A lifecycle model where the agent survives to the next period with probability \(s_h\) (age-dependent) or dies and leaves a bequest. Unlike the retirement choice (§3.5, §6.1), where a discrete decision changes the period type, mortality branching is resolved within each period: a bequest terminal stage evaluates the death payoff locally, keeping the nest sequential.

Key design principle: when a branch has a terminal resolution (bequest, absorbing state) that can be evaluated locally, handle it as an intra-period terminal stage rather than routing to a shared terminal period. This avoids exponential path proliferation and keeps the nest a simple chain.

Bellman equation (within each period):

\[ \mathrm{v}_t(w) = \max_c \left\{ u(c) + \beta \left[ s_t \cdot \mathrm{v}_{t+1}(a') + (1 - s_t) \cdot \mathrm{b}(a') \right] \right\} \]

where \(a' = w - c\), \(\mathrm{v}_{t+1}\) is the next-period continuation value (via inter-period connector), and \(\mathrm{b}(a) = \theta \log(a)\) is the bequest function.

Period structure (DAG within each period):

[consumption] → [mortality_branching]
                   ├── survive: a       → (next period via connector)
                   └── die:     a_death → [bequest_terminal]

Backward solve order: (1) bequest_terminal, (2) mortality_branching (combines survive continuation from next period + bequest value), (3) consumption.

Stage: MortalityBranching (branching, expectation)

A minimal branching stage with exogenous survival probability. Identity arrival, pass-through to both branches, and a weighted-sum mover.

name: MortalityBranching
kind: branching

symbols:
  spaces:
    Xa: "@def R+"

  prestate:
    a: "@in Xa"           # end-of-period assets (from consumption stage)

  states:
    a: "@in Xa"

  poststates:
    survive:
      a: "@in Xa"
    die:
      a_death: "@in Xa" #note the new variable name here — branching occurred!

  exogenous: {}

  values:
    V[<]: "@in R"
    V: "@in R"

    # Branch-keyed continuation value family (label → value symbol name).
    # Keys must match `poststates.{label}` and `dcsn_to_cntn_transition.{label}`.
    V[>]:
      survive: V_survive
      die: V_die

    V_survive: "@in R"    # next-period continuation value
    V_die: "@in R"        # bequest value

  parameters: [surv_prob]

equations:
  arvl_to_dcsn_transition: |
    a = a[<]

  #note below that the common name fans out in fwd sense.
  dcsn_to_cntn_transition:
    survive: |
      a[>] = a

    die: |
      a_death[>] = a

  cntn_to_dcsn_mover:
    Bellman: |
      V = surv_prob * V_survive[>] + (1 - surv_prob) * V_die[>]

  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = V

Stage: BequestTerminal (intra-period terminal)

A leaf stage that evaluates the bequest function directly. No continuation perch — the mover assigns \(V\) from the state without referencing \(V^{>}\).

name: BequestTerminal

symbols:
  spaces:
    Xa: "@def R+"

  prestate:
    a_death: "@in Xa"

  states:
    a: "@in Xa"

  poststates: {}
  controls: {}
  exogenous: {}

  values:
    V[<]: "@in R"
    V: "@in R"

  parameters: [theta]

equations:
  arvl_to_dcsn_transition: |
    a = a_death[<]

  cntn_to_dcsn_mover:
    Bellman: |
      V = theta * log(a)     # terminal value — no continuation
  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = V

Period template:

name: WorkingPeriodWithMortality

stages:
  - consumption: !stage            # consumption-savings (ConsumptionSavings template)
  - mortality: !stage              # branching: survive vs die (MortalityBranching)
  - bequest: !stage                # die-branch terminal (BequestTerminal)

Wiring is implicit: consumption outputs a (poststate), which matches mortality's prestate a. The mortality.die branch outputs a_death (a new name — the coproduct created a distinct field), which matches bequest's prestate a_death. The mortality.survive branch outputs a, which is the period's outgoing field for the inter-period connector.

Nest structure (sequential — no DAG needed):

working[0] → working[1] → retirement → terminal
name: lifecycle_with_mortality

periods:
  - working_period: !period
  - working_period: !period
  - retirement_period: !period
  - terminal: !period

connectors:
  - {a: b}              # working[0].survive → working[1]
  - {a: b}              # working[1].survive → retirement
  - {a: b}              # retirement.survive → terminal

Each period handles death internally via its bequest terminal stage. The survive branch's output a feeds through the inter-period connector to the next period's prestate b. The nest's terminal period provides the final continuation value for the last period's survive branch — it is not the same as the intra-period bequest terminal stages.

6.3 Portfolio choice with taste shocks (within-stage discrete choice, AGGREGATE)

This example is motivated by the Eggsandbaskets lifecycle model (AI/context/econ-applications/). An agent allocates wealth across \(K\) discrete portfolio shares \(\pi_k \in \{\pi_1, \ldots, \pi_K\}\). Each share leads to a different return but the same continuation state space \((a', y)\).

This is NOT branching

All portfolio shares lead to the same state space \((a', y)\) and the same successor stage. The discrete variable \(\pi\) is an arg — it parameterizes the dcsn_to_cntn_transition and is aggregated by AGGREGATE_pi, not optimized by max. The rule of thumb from §8 applies: same downstream state space → within-stage operator (max_c, AGGREGATE_pi), not branch-keyed poststates. Compare with housing \(H' \in \mathcal{H}\) in housing_choice_mdp.md.

Stage: PortfolioChoice (within-stage discrete choice)

  • Arrival: \((a, y_\text{pre})\) — liquid assets, pre-shock income
  • Decision: \((a, y)\) — after income shock realization
  • Continuation: \((a', y)\) — where \(a'_k = R_k (a - c)\) depends on the discrete arg \(\pi_k\)
  • pi is declared as an arg — it passes through the transition so the backward mover knows what to iterate over

Bellman equation:

\[ \mathrm{v}(a, y) = \max_c \left\{ u(c) + \beta \sum_{k=1}^{K} p(k \mid a, y) \, \mathrm{v}_{\succ}\!\bigl(\mathrm{g}_{\sim\succ}(a, c, \pi_k),\, y\bigr) \right\} \]

where \(p(k \mid a, y)\) is generated by AGGREGATE (methodized as softmax: \(p_k = \exp(\mathrm{v}_{\succ,k} / \sigma_\pi) / \sum_l \exp(\mathrm{v}_{\succ,l} / \sigma_\pi)\)), and \(\mathrm{v}_{\succ}\) is a single continuation value function evaluated at each portfolio-specific return \(\mathrm{g}_{\sim\succ}(a, c, \pi_k) = (1 + r + \pi_k(R_{\text{equity}} - r))(a - c)\).

Euler equation (envelope condition):

\[ u'(c) = \beta \sum_{k=1}^{K} p(k \mid a, y) \, R_k \, u'(c'_k) \]

The marginal value is a probability-weighted average over the discrete portfolio values — the same functional form as exogenous expectation, but with endogenous probabilities. No new operator is needed in the Euler equation.

Stage YAML (variant A — return shock deferred to next stage):

In this variant, the portfolio return is not realized within this stage. The return R_equity is a parameter (known rate), and pi determines the portfolio allocation. The subscript in AGGREGATE_pi binds the discrete arg variable pi (declared in symbols.args) and tells the backward mover to iterate over X_pi by reading the transition: for each pi_k, it computes a_nxt(pi_k) via dcsn_to_cntn_transition and evaluates V[>] at the result.

stage:
  name: "PortfolioChoice_ShockNextStage"
  # NOT branching — pi is a discrete arg, same state space for all values
  # Return shock deferred to next stage

symbols:
  spaces:
    Xa: "@def R+"
    XY: "@def {1, ..., n_y}"
    X_pi: "@def {pi_1, pi_2, pi_3}"
  prestate:
    a: "@in Xa"
    y_pre: "@in XY"
  states:
    a: "@in Xa"
    y: "@in XY"
  poststates:
    a_nxt: "@in Xa"
    y: "@in XY"
  controls:
    c: "@in Xa"
  args:                                    # discrete variables aggregated by AGGREGATE
    pi: "@in X_pi"                         # portfolio share — not a max control
  exogenous:
    y:
      - "@in XY"
      - "@dist MarkovChain(Pi_y, y_vals)"
  values:
    V[<]: "@in R"
    V: "@in R"
    p: "@in simplex(3)"                    # endogenous choice probabilities
  parameters: [beta, r, sigma_pi, R_equity]

equations:
  dcsn_to_cntn_transition: |
    a_nxt[>] = (1 + r + pi*(R_equity - r))*(a - c)
    y[>] = y

  cntn_to_dcsn_mover:
    Bellman: |
      p = AGGREGATE_pi(V[>]; sigma_pi)
      V = max_c{u(c) + beta * E_pi[p; V[>]]}

  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = E_y(V)

The desugaring of AGGREGATE_pi(V[>]; sigma_pi): the backward mover reads pi from the transition, iterates over \(X_\pi = \{\pi_1, \pi_2, \pi_3\}\), computes \(a_{\text{nxt},k} = (1 + r + \pi_k(R_{\text{equity}} - r))(a - c)\) for each \(k\), evaluates \(V^{>}(a_{\text{nxt},k}, y)\), and applies the kernel (softmax etc.) to the resulting vector to produce \(p\).

Stage YAML (variant B — return shock realized this period):

Here the return shock R_equity is an exogenous variable realized between the decision and continuation perches (within this stage). The cntn_to_dcsn_mover must take expectation over R_equity (via E_R) and aggregate over pi (via AGGREGATE_pi). The AGGREGATE sees the expected continuation value \(\mathbb{E}_R[V^{>}]\) for each \(\pi_k\), not the raw \(V^{>}\).

stage:
  name: "PortfolioChoice_ReturnsThisPeriod"
  # NOT branching — pi is a discrete arg, same state space for all values
  # Return shock R_equity realized between dcsn and cntn

symbols:
  spaces:
    Xa: "@def R+"
    XY: "@def {1, ..., n_y}"
    X_pi: "@def {pi_1, pi_2, pi_3}"
    XR: "@def {R_1, ..., R_m}"
  prestate:
    a: "@in Xa"
    y_pre: "@in XY"
  states:
    a: "@in Xa"
    y: "@in XY"
  poststates:
    a_nxt: "@in Xa"
    y: "@in XY"
  controls:
    c: "@in Xa"
  args:
    pi: "@in X_pi"
  exogenous:
    y:
      - "@in XY"
      - "@dist MarkovChain(Pi_y, y_vals)"
    R_equity:                              # return shock realized THIS period
      - "@in XR"
      - "@dist Distribution(P_R)"
  values:
    V[<]: "@in R"
    V: "@in R"
    p: "@in simplex(3)"
  parameters: [beta, r, sigma_pi]

equations:
  dcsn_to_cntn_transition: |
    a_nxt[>] = (1 + r + pi*(R_equity - r))*(a - c)
    y[>] = y

  cntn_to_dcsn_mover:
    Bellman: |
      p = AGGREGATE_pi(E_R[V[>]]; sigma_pi)
      V = max_c{u(c) + beta * E_pi[p; E_R[V[>]]]}

  dcsn_to_arvl_mover:
    Bellman: |
      V[<] = E_y(V)

The key difference: both pi and R_equity pass through dcsn_to_cntn_transition. The backward mover desugars as: for each \(\pi_k\), for each \(R_j\), compute \(a_{\text{nxt}}(\pi_k, R_j)\) and evaluate \(V^{>}\). Then \(E_R\) integrates out the return dimension (exogenous expectation), and \(\text{AGGREGATE}_\pi\) applies softmax to the resulting expected values \([\mathbb{E}_R V^{>}(\pi_1), \ldots, \mathbb{E}_R V^{>}(\pi_K)]\) to produce \(p\).

args: — a new symbol role #todo/syntax #aggregation

The args: section declares discrete variables that parameterize the dcsn_to_cntn_transition and are aggregated by an AGGREGATE operator in the backward mover. An arg is:

  • not a control (not optimized by max_c — AGGREGATE handles it via soft/probabilistic choice)
  • not an exogenous shock (the probabilities are endogenous, computed from continuation values)
  • not a poststate (the successor stage does not receive it — it is consumed by the transition and integrated out by AGGREGATE + E)

Binding convention (SYM / Dolang+)

  • Accept a form that binds the discrete index variable: AGGREGATE_{d @\in SET}(…) (or AGGREGATE_{d∈SET}(…)).
  • This is unambiguous: it tells you which axis you’re aggregating over.
  • Do not accept AGGREGATE_{SET}(…) as a general surface form.
  • Without binding a variable, it’s ambiguous (which symbol in the transition/expression is ranging over SET?), unless we also introduce a stronger “typed vector over SET” notion in the SYM IR.
  • Canonical in dolo-plus: keep the binder as the subscripted variable, and let the set come from the declaration (so no extra arg= kw and no redundancy):
  • Declare the index: symbols.args.d: "@in SET"
  • Use: p = AGGREGATE_d(expr; params…)

This is exactly why AGGREGATE_pi(…) pairs cleanly with E_pi[p; …]: both are bound by the same pi, and SET is already known from pi: "@in X_pi".

Branching vs within-stage discrete choice

These examples demonstrate why portfolio choice does not require branching: all \(\pi\) values produce the same poststate schema (a_nxt, y) and route to the same successor stage. The coproduct collapses to a product \(\{1,\ldots,K\} \times X_c\) — a discrete index, not a disjoint union. Use branching (§6.1, §6.2) only when choices lead to qualitatively different downstream paths (different state spaces, different stages).

7. Validation rules

7.1 Branching stage validation

  1. A stage with kind: "branching" must declare at least two continuation perches (named sub-blocks) in poststates.
  2. Each continuation perch must have a unique name.
  3. The decision mover must define V using at least two branch continuation values, referenced via V[>][{label}] (canonical sugar) or explicitly via V_{label}[>] (e.g. V_own[>], V_rent[>]) where {label} corresponds to a {label}: sub-block under poststates.
  4. For weighted-sum branching: any weight/probability variables used in the sum (e.g. surv_prob, p) must be declared (as parameters:, exogenous:, or typed values:). If p is computed endogenously, it must be assigned in the mover equations (e.g. p = AGGREGATE(...)) before it is used.

7.2 Namespace composition validation

  1. No overloading: within a period namespace, each variable name refers to exactly one quantity. Two stages may not output different things under the same name.
  2. Each continuation perch's output fields must match a successor stage occurrence's arrival fields by name (after applying any connectors).
  3. No two continuation perches may map to the same successor stage (fan-out, not fan-in within a period).
  4. Connectors are injective rename maps (no two source names map to the same target name).
  5. Connector compatibility: after applying any connector, field types must agree.
  6. Unique-producer rule: for any stage arrival field \(x\) within a period, there must be exactly one upstream continuation field that supplies it (after applying any renaming connectors). Fan-in (recombination) must not be automatic via name matching — require an explicit merge/join stage or an inter-period connector to map branch-qualified outputs into a single arrival record.

7.3 Nest validation

  1. Inter-period connectors must reference valid adjacent period pairs.
  2. No self-loops: a connector cannot target the source period itself (within finite-horizon models).
  3. The nest graph must be acyclic (for finite-horizon models).

7.4 DAG acyclicity within periods

Within a branching period, the stage graph (stages as nodes; namespace-induced wiring edges after applying any connectors) must be a DAG. This ensures a valid topological sort for backward induction.

8. Relationship to existing within-stage discrete choice

It is important to distinguish branching (this spec) from within-stage discrete choice (already supported):

Within-stage discrete choice Branching
Example Choose \(H' \in \mathcal{H}\) (housing size) Choose own vs. rent (tenure)
State spaces Same state space for all choices Different state space per branch
Value function Single \(\mathrm{v}(x)\) with \(\max_{c}\) in mover Separate \(\mathrm{v}_{\succ,j}(x_j)\) per branch
YAML max_c{...} in cntn_to_dcsn_mover {label}: sub-blocks in poststates + a cntn_to_dcsn_mover.Bellman equation that combines V_{label}[>] terms
Continuation One continuation perch (poststates is flat) Multiple continuation branches (poststates.{label})
Successor stages Same successor stage Different successor stages per branch
Period wiring syntax Standard (implicit by namespace + connectors) Implicit by namespace + connectors (no overloading; underloading OK)

Rule of thumb: if all choices lead to the same downstream state space and stage sequence, use within-stage max_c{}. If choices lead to qualitatively different downstream paths (different state spaces, different stages), use branching.

9. Open questions and deferred items

  1. Condition syntax for max branching: How to express branch conditions (e.g., d == own) in Dolang+? Current direction: the branch selector is a declared control variable; conditions are implicit from the max over branch labels. #todo/syntax

  2. Nested branching: A branch target that is itself a branching stage. Algebraically valid (nested coproduct), but solver support is complex. Permitted in the syntax; solver implementation deferred. #todo/solver

  3. Merging (fan-in): Multiple branches converging to a single successor stage (e.g., both own and rent paths lead to the same next-period tenure choice). Currently handled at the period/connector boundary. Within-period fan-in may need a JoinMorphism in the categorical view (see AI/prompts/AAS/03012025/final-report/source-reports/stage-morphisms-composition-parser.md). #todo/composition

  4. Terminal branches: How to specify a terminal condition (bequest, absorbing state) as a branch target. Current direction: terminal is a period with a specified terminal value function. #todo/syntax

  5. Infinite-horizon branching: Branching within a stationary (infinite-horizon) model requires loop-closure semantics. Deferred. #todo/theory

  6. Representation in dolo SymbolicModel: How branching stages map to the existing SymbolicModel class. Options: (a) multiple poststates groups, (b) a branches metadata field, (c) a new stage subclass. To be resolved during implementation. #todo/implementation

  7. Connector-to-continuation-perch association: In a nest with inter-period connectors, each connector maps continuation fields of one period to arrival fields of the next. The association is determined by namespace matching (field names in the rename dict). #todo/syntax

  8. Probability vector type: Remaining open question: what canonical domain syntax should we use for probability vectors (@in simplex(K) or equivalent)? See §3.7 for discussion. #todo/syntax #endogenous-probability

  9. General probability kernels beyond softmax: The mover equations can compute probabilities from branch values in many ways. Softmax (Gumbel taste shocks) is the canonical closed-form case. Other kernels (probit, nested logit/GEV) could also be supported. Should the spec enumerate supported kernels or allow arbitrary expressions? #todo/theory

  10. SYM vs methodization boundary for taste shocks: The AGGREGATE operator is declared at the SYM level; its specific kernel (softmax, probit, etc.) is a methodization choice. This resolves the Υ/Ρ boundary: the existence of probability aggregation is Υ-level, the form of the kernel is Ρ-level. The Dyn-X precedent uses smooth_sigma as a solver parameter, consistent with this split. #todo/syntax #Υ-Ρ-boundary

10. Summary of new constructs

Construct Sequential (spec 0.1h) Branching (this spec)
Intra-period connector {from, to, rename} edge (spec 0.1h) Period-wide rename dict keyed by stage occurrence
Inter-period connector Positional rename dict Rename dict between adjacent periods
Stage continuation Single continuation perch Multiple named continuation branches (coproduct at dcsn→cntn)
Backward mover Bellman combines V with a single continuation object Bellman combines multiple branch continuation values V_{label}[>] (via max_d{…} or weighted sum); probabilities may be exogenous or computed via AGGREGATE
Forward operator Direct push-forward Population splitting by branch
Solve order Linear (reverse of stage list) Topological sort of stage DAG
Period graph Path (ordered list) DAG of stages + connectors
Nest graph Path (ordered list) Linear chain (branching is internal to periods)

The key insight: branching is a stage-level concept (multiple continuation perches + a mover equation that combines multiple branch values). The coproduct occurs between the decision and continuation perches. Within-period wiring remains namespace-driven (no overloading; underloading OK), with period-wide connectors (rename maps on stage occurrences) to adapt reusable templates.

11. Required package changes (packages/dolo, packages/dolang)

11.1 Dolang+ (packages/dolang/)

  1. Chained subscript V[>][label]: extend the grammar to parse chained [...] after a perch-tagged name. The first [...] remains the perch index; any additional [...] are tensor/index selection. Required edits: grammar.lark (parse rule), grammar.py (printer/roundtrip).
  2. AGGREGATE_d(expr; params) binder: accept the canonical variable-binding form where d is a declared symbols.args variable. Do not accept AGGREGATE_{SET}(…) (no bound variable → ambiguous).

11.2 dolo-plus (packages/dolo/)

  1. Parse symbols.args with discrete/finite domain entries ({name: “@in FiniteSet”}).
  2. Parse branch-keyed poststates.{label} sub-blocks: emit a flattened poststate list (for perch inference) plus a branch structure sidecar stage.branch_poststates.
  3. Parse branch-keyed dcsn_to_cntn_transition.{label}: expose as stage.dcsn_to_cntn_transitions_by_branch keyed by label.
  4. Parse branch-keyed values.V[>].{label}: validate label-set consistency across poststates, values.V[>], and dcsn_to_cntn_transition.

11.3 Docs

Update docs/dolo-plus-spec/syntax-semantic-rules/05-periods-models.md to reference this spec (replace “Branching (deferred)” note with a cross-link to spec 0.1l).

References

  • Spec 0.1h — Period and nest instantiation (baseline)
  • docs/theory/ddsl-foundations/periods-nests-theory.md — Graph-theoretic foundations
  • docs/dolo-plus-spec/syntax-semantic-rules/05-periods-models.md — Period/model syntax
  • AI/working/AAS/29012026/branching-explore.md.md — Conceptual draft
  • AI/context/external/ModularMDP-repos/Dyn-X/ — Dyn-X housing tenure choice implementation
  • AI/context/external/ModularMDP-repos/Dyn-X/project-notes/prompts/context/unified/sections/recursive-problem-definition.md — Branching Bellman equations
  • AI/context/external/ModularMDP-repos/Dyn-X/project-notes/prompts/context/unified/sections/population-dynamics.md — Population dynamics with branching
  • AI/prompts/AAS/03012025/final-report/source-reports/stage-morphisms-composition-parser.md — Categorical IR with Branch and JoinMorphism
  • AI/context/econ-applications/ — Eggsandbaskets3.0 lifecycle model (portfolio choice with logit aggregation)