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 branchexpectation(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:orexogenous:(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
maxandexpectationcan be unified as the choice over a probability density. Themaxoperator chooses the degenerate measure on the best branch;expectationevaluates a given measure. When that measure is itself derived from the values (endogenousAGGREGATE), the probability generation step precedes the expectation — the aggregation mode remainsexpectation.
#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:
- 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).
- Value functions are separable across the discrete index: each regime has its own named value function and different continuous state space.
- 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:
poststatesbecomes branch-keyed using{label}:sub-blocks (e.g.own:,rent:) whenkind: branching.dcsn_to_cntn_transitionbecomes branch-keyed using{label}: |sub-blocks (e.g.own: |,rent: |) whenkind: 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 toV_own_func[>],V_rent_func[>]) and combine them withmax_d{own,rent}or a weighted sum. - If the branch specific cntn value function is named, a valid alternative is
V_own[>]soV_own[>] =V[>][own], but the nameV_ownis not implicit, it is specified by declaration thatV_ownis 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:
owncontinues with(a, H, y_own)rentcontinues 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:
poststatescontains{label}:sub-blocks, one per branch. Each sub-block is a distinct continuation perch with its own state space.- Branch labels (e.g.
own,rent) are reused consistently: - as keys under
poststates(own:,rent:), - as keys under
equations.dcsn_to_cntn_transition(own:,rent:, see §3.2), - as keys under
values.V[>](own: V_own,rent: V_rent). - The branch-label set must be consistent across the branch-keyed sub-blocks:
poststates.{label},- the keys of
values.V[>](and, for each label, the mapped symbolvalues.V[>].{label}must be declared undervalues:), and equations.dcsn_to_cntn_transition.{label}(and anymax_d{…}/argmax_d{…}over branches).- 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 invalues:) and are referenced in equations either explicitly asV_own[>],V_rent[>]or using the canonical sugarV[>][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:
- Compute \(p(j)\) from the branch value functions (inside the mover equations)
- Use the computed \(p(j)\) as weights in a standard weighted sum
The general branching equation (adapted from Dyn-X, in conventions notation):
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. AGGREGATEis 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-choicepis an ordinary declared variable (recommended: invalues:) 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
-
Type of
p: We declarepas a typed field (recommended: invalues:). What is the canonical domain syntax for probability vectors —@in simplex(K)or something else? -
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? -
Relationship to the Dyn-X
smooth_sigma: In the Dyn-X codebase,smooth_sigmais 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 viaAGGREGATE),
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:
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:
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):
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):
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\)
piis declared as anarg— it passes through the transition so the backward mover knows what to iterate over
Bellman equation:
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):
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}(…)(orAGGREGATE_{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 overSET” 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 extraarg=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¶
- A stage with
kind: "branching"must declare at least two continuation perches (named sub-blocks) inpoststates. - Each continuation perch must have a unique name.
- The decision mover must define
Vusing at least two branch continuation values, referenced viaV[>][{label}](canonical sugar) or explicitly viaV_{label}[>](e.g.V_own[>],V_rent[>]) where{label}corresponds to a{label}:sub-block underpoststates. - For weighted-sum branching: any weight/probability variables used in the sum (e.g.
surv_prob,p) must be declared (asparameters:,exogenous:, or typedvalues:). Ifpis computed endogenously, it must be assigned in the mover equations (e.g.p = AGGREGATE(...)) before it is used.
7.2 Namespace composition validation¶
- 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.
- Each continuation perch's output fields must match a successor stage occurrence's arrival fields by name (after applying any connectors).
- No two continuation perches may map to the same successor stage (fan-out, not fan-in within a period).
- Connectors are injective rename maps (no two source names map to the same target name).
- Connector compatibility: after applying any connector, field types must agree.
- 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¶
- Inter-period connectors must reference valid adjacent period pairs.
- No self-loops: a connector cannot target the source period itself (within finite-horizon models).
- 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¶
-
Condition syntax for
maxbranching: 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 themaxover branch labels. #todo/syntax -
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
-
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
JoinMorphismin the categorical view (seeAI/prompts/AAS/03012025/final-report/source-reports/stage-morphisms-composition-parser.md). #todo/composition -
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
-
Infinite-horizon branching: Branching within a stationary (infinite-horizon) model requires loop-closure semantics. Deferred. #todo/theory
-
Representation in dolo SymbolicModel: How branching stages map to the existing
SymbolicModelclass. Options: (a) multiplepoststatesgroups, (b) abranchesmetadata field, (c) a new stage subclass. To be resolved during implementation. #todo/implementation -
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
-
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 -
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
-
SYM vs methodization boundary for taste shocks: The
AGGREGATEoperator 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 usessmooth_sigmaas 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/)¶
- 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). AGGREGATE_d(expr; params)binder: accept the canonical variable-binding form wheredis a declaredsymbols.argsvariable. Do not acceptAGGREGATE_{SET}(…)(no bound variable → ambiguous).
11.2 dolo-plus (packages/dolo/)¶
- Parse
symbols.argswith discrete/finite domain entries ({name: “@in FiniteSet”}). - Parse branch-keyed
poststates.{label}sub-blocks: emit a flattened poststate list (for perch inference) plus a branch structure sidecarstage.branch_poststates. - Parse branch-keyed
dcsn_to_cntn_transition.{label}: expose asstage.dcsn_to_cntn_transitions_by_branchkeyed by label. - Parse branch-keyed
values.V[>].{label}: validate label-set consistency acrosspoststates,values.V[>], anddcsn_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 foundationsdocs/dolo-plus-spec/syntax-semantic-rules/05-periods-models.md— Period/model syntaxAI/working/AAS/29012026/branching-explore.md.md— Conceptual draftAI/context/external/ModularMDP-repos/Dyn-X/— Dyn-X housing tenure choice implementationAI/context/external/ModularMDP-repos/Dyn-X/project-notes/prompts/context/unified/sections/recursive-problem-definition.md— Branching Bellman equationsAI/context/external/ModularMDP-repos/Dyn-X/project-notes/prompts/context/unified/sections/population-dynamics.md— Population dynamics with branchingAI/prompts/AAS/03012025/final-report/source-reports/stage-morphisms-composition-parser.md— Categorical IR withBranchandJoinMorphismAI/context/econ-applications/— Eggsandbaskets3.0 lifecycle model (portfolio choice with logit aggregation)