Skip to content

Meaning Map to Code (Υ → Ρ)

Companion note for the port-cons FFP notebook

V[>] as a Backus object

In Backus's FFP (1978), there is a clean separation between objects (syntactic atoms that don't carry their own meaning), functions (well-formed expressions built from objects), and the definitional system (Def f ≡ r) that assigns meaning to names.

V[>] in a stage YAML is exactly this kind of object. It's a string — a perch-qualified symbol sitting in the YAML file. By itself it means nothing. It becomes a well-formed function object only when the definitional system D processes it: the grammar checks that V[>] is well-typed (a value at the continuation perch), and the stage context supplies the transition g that connects decision to continuation.

The Υ (meaning) map then assigns mathematical semantics to this function object:

V[>]              →  Υ  →  V_cont(g(m, c))      →  Ρ  →  np.interp(...)
string in YAML         mathematical expression        executable code
(object)               (meaning)                      (representation)

This is the Backus architecture applied to Bellman problems: objects are syntactic, meaning comes from the definitional system, and computation is a separate map (Ρ) that requires methodization and calibration on top of the meaning.

The whisperer

The notebook's make_stage_operator factory — the whisperer — performs both maps in sequence:

  1. Read the equations from the stage YAML (objects in O)
  2. Resolve perch tags via the stage context, applying Υ (objects → meaning)
  3. Read the method tag (!egm or !vfi) and compile into callables, applying Ρ (meaning → code)
  4. Return a stage operator: ValueFn → ValueFn

The whisperer returns an operator, not a solution. The solver calls the operator to find the fixed point.

Matsya as the interpretation layer

The notebook calls Matsya at runtime, inside the solver pipeline to perform the Υ map. The full stage YAML — including perch tags, symbol declarations, spaces, transitions, and mover sub-equations — is sent to Matsya with a binding context:

  • V_cont(x) is what V[>] means in code (a callable)
  • dV_cont(x) is what dV[>] means in code (marginal of the callable)
  • a is the poststate grid variable
  • m_d is the decision grid variable

Matsya reads the perch tags, finds the transitions that connect perches, substitutes, and returns a structured JSON recipe — Python expressions keyed by DDSL equation block names:

{
  "vfi_maximand":            "(c**(1-rho))/(1-rho) + beta*V_cont(m_d - c)",
  "inv_euler":               "(beta*dV_cont(a))**(-1/rho)",
  "cntn_to_dcsn_transition": "a + c",
  "marginal_bellman":        "c**(-rho)",
  "dcsn_to_cntn_transition": "m_d - c",
  "utility":                 "(c**(1-rho))/(1-rho)"
}

The whisperer then compiles these returned strings into numpy callables via eval(). Matsya executes the definitional system — it doesn't define it.

What this means architecturally

The pipeline becomes:

syntax  →  whisperer + Matsya (Υ interpretation)  →  code (Ρ compilation)

Matsya substitutes for hard-wired, solver-level interpretation of the dolo-plus syntax. We don't need the solver to understand perch tags, bracket notation, or transition structure. The whisperer sends the raw YAML to Matsya, gets back de-sugared expressions in the target variable namespace, and compiles them. The solver receives callables — it never touches equation strings.

This decouples the equation compilation from the solver logic. We can code the solver however we want; the Υ map is handled externally.

The recipe as structured Υ output

The recipe returned by Matsya has a specific structure worth examining. Each key is a DDSL equation block name, and each value is a Python expression. The keys correspond to the block names declared in the stage YAML's equations: section:

Recipe key Stage YAML source What it means
dcsn_to_cntn_transition a = m_d - c Forward transition \(g\): decision → continuation
cntn_to_dcsn_transition m_d[>] = a + c[>] Reverse transition (EGM): continuation → decision
inv_euler c[>] = (β*dV[>])^(-1/ρ) Inverse Euler equation
marginal_bellman dV = (c)^(-ρ) Envelope condition
utility u = (c^(1-ρ))/(1-ρ) Flow utility
vfi_maximand V = max_{c}(u + β*V[>]) De-sugared VFI objective

The de-sugaring that Matsya performs is visible in the vfi_maximand: the YAML says u + β*V[>], and Matsya returns (c**(1-rho))/(1-rho) + beta*V_cont(m_d - c) — substituting the utility definition for u, the transition m_d - c for the argument of V[>], and the code-level callable V_cont for the perch-tagged symbol.

Unicode–ASCII parameter aliasing

A practical issue at the Ρ level: the stage YAML declares parameters using Unicode (β, ρ), which is what the calibration functor produces. But recipe expressions use ASCII (beta, rho) because that's what Matsya returns (and what Python can evaluate without Unicode identifier support issues in some contexts).

The whisperer bridges this with an alias map when building the eval namespace:

_UNICODE_TO_ASCII = {'β': 'beta', 'ρ': 'rho', ...}

Both forms are present in the namespace, so recipe expressions resolve correctly regardless of which convention the RAG assistant or the stage YAML uses.

Composition and accretion

Stages compose as functions: a period is backward composition of its stages (V = S_cons(S_port(V_cont))). A connector is a rename between adjacent components. The nest is an interleaved sequence of periods and connectors, built backward one period at a time (accretion).

Method independence

The stage YAML declares all sub-equations for any method. The methods YAML selects which ones the whisperer compiles. Swap !egm for !vfi and only the Ρ-level changes — the Υ image is identical. The notebook verifies this: both methods converge to the same value functions (differences at the 4th decimal place, consistent with VFI grid-search discretization).

The same Matsya recipe serves both methods. The EGM operator uses inv_euler, cntn_to_dcsn_transition, marginal_bellman (the EGM-specific sub-equations). The VFI operator uses vfi_maximand (the fully de-sugared objective). Both read from the same recipe dict — the method tag determines which keys are consumed, not which expressions exist.

Open question: recipe expression scope #ambiguity

When Matsya returns dcsn_to_cntn_transition: m_d - c, what does c refer to?

In the current implementation, recipe expressions are composable fragments evaluated sequentially in a namespace where previous results are bound. The EGM operator computes c from the inverse Euler first, binds it into the namespace, and then evaluates m_d - c against that binding. The c in the recipe is a bare variable, not the inverse Euler expression.

But this raises a Υ-level question. The stage YAML declares:

InvEuler: |
  c[>] = (β*dV[>])^(-1/ρ)
cntn_to_dcsn_transition: |
  m_d[>] = a + c[>]

Here c[>] in the reverse transition is explicitly the consumption at the continuation perch — the same object defined by the inverse Euler. Should Matsya substitute it, producing m_d = a + (beta*dV_cont(a))**(-1/rho)? Or leave c bare, trusting the sequential evaluation to bind it?

The answer may be method-dependent: EGM naturally evaluates fragments sequentially (compute c, then use it), while VFI needs the fully-composed maximand (everything substituted into one expression). If so, the Υ map produces the same mathematical objects regardless of method, but the recipe format (composable fragments vs closed forms) is a Ρ-level decision.

This is an open design question — see the roadmap todo for the current status.