7. Execution Pipeline¶
This section describes the complete execution pipeline that transforms DDSL models from specification to numerical results, with explicit attention to the ordering of methodization before calibration and the threading of Υ and ρ through each stage.
7.1 Pipeline Overview¶
The DDSL execution pipeline consists of eight stages:
Each stage transforms the model representation:
| Stage | Input | Output | Map Used |
|---|---|---|---|
| 1. Parse | YAML text | Syntactic objects | — |
| 2. Validate | Syntactic objects | Validated syntax | — |
| 3. Mathematical meaning | Validated syntax | Mathematical model | Υ |
| 4. Methodize | Validated syntax | Methodized stage | M |
| 5. Calibrate | Methodized stage | Calibrated stage | C |
| 6. Represent | Calibrated stage | Computational objects | ρ |
| 7. Solve | Computational objects | Numerical solution | — |
| 8. Analyze | Numerical solution | Economic insights | — |
Critical ordering: Methodization (stage 4) must occur before calibration (stage 5).
7.2 Stage 1: Parsing¶
YAML Parser¶
The parser transforms YAML files into syntactic objects:
def parse_ddsl(yaml_file):
with open(yaml_file, 'r') as f:
raw = yaml.safe_load(f)
# Parse stage metadata
stage_meta = parse_stage_meta(raw['stage'])
# Parse symbols block (including operator registry references)
symbols = parse_symbols(raw['symbols'])
# Parse perches with mover definitions
perches = parse_perches(raw['perches'])
# Parse equations
equations = parse_equations(raw['equations'])
return SyntacticStage(stage_meta, symbols, perches, equations)
Operator Registry Recognition¶
During parsing, operator usages are identified and linked to the registry:
def parse_mover_body(body_text):
"""Parse mover @via body, identifying registry operator usages."""
operators_used = []
# Find expectation operators: E_w, E_ε, etc.
for match in re.finditer(r'E_(\w+)\[', body_text):
operators_used.append(('expectation', match.group(1)))
# Find optimization operators: argmax_{...}, max_{...}
for match in re.finditer(r'argmax_\{([^}]+)\}', body_text):
operators_used.append(('argmax', match.group(1)))
# Find draw operators: x ~ D
for match in re.finditer(r'(\w+)\s*~\s*(\w+)', body_text):
operators_used.append(('draw', match.group(1), match.group(2)))
return operators_used
7.3 Stage 2: Validation¶
Syntactic Validation¶
Check that all symbols are properly declared and all operator usages are valid:
def validate_syntax(stage):
errors = []
# Check all referenced symbols are declared
for eq in stage.equations:
for symbol in extract_symbols(eq):
if not is_declared(symbol, stage.symbols):
errors.append(f"Undefined symbol: {symbol}")
# Check type consistency
for func in stage.functions:
if not check_type_consistency(func, stage.spaces):
errors.append(f"Type error in function: {func.name}")
# Check operator registry coverage
for mover in stage.movers:
for op in mover.operators_used:
if not is_registered_operator(op):
errors.append(f"Unknown operator in {mover.name}: {op}")
# Check perch connectivity
for perch in stage.perches:
if not check_perch_connectivity(perch, stage.movers):
errors.append(f"Disconnected perch: {perch.name}")
if errors:
raise ValidationError(errors)
return ValidatedStage(stage)
7.4 Stage 3: Mathematical Meaning (Υ)¶
Building the Mathematical Model¶
At this stage, we construct the mathematical interpretation of the model:
def build_mathematical_model(validated_stage, param_values=None):
"""
Apply Υ to get the mathematical model.
Parameters can be provided or left symbolic.
Methods and settings are NOT needed at this stage.
"""
math_model = MathematicalModel()
# Map spaces to mathematical spaces
for space in validated_stage.spaces:
math_model.spaces[space.name] = upsilon_space(space)
# e.g., Υ(Xa @def R+) = ℝ₊
# Map functions to mathematical functions
for func in validated_stage.functions:
math_model.functions[func.name] = upsilon_function(func, math_model.spaces)
# e.g., Υ(g_av: Xa × W -> Xv) = g_av: ℝ₊ × ℝ → ℝ₊
# Map movers to mathematical operators
for mover in validated_stage.movers:
math_model.operators[mover.name] = upsilon_mover(mover, math_model)
# e.g., Υ(B_arvl) = conditional expectation operator
# If parameters provided, substitute them
if param_values:
math_model = substitute_parameters(math_model, param_values)
return math_model
Υ for Movers¶
def upsilon_mover(mover, math_model):
"""
Map a syntactic mover to its mathematical operator.
This is the TRUE mathematical operator, not an approximation.
"""
if mover.type == 'backward':
if 'E_' in mover.body:
# Conditional expectation mover
return ConditionalExpectationOperator(
conditioning_variable=extract_conditioning_var(mover),
integrating_variable=extract_shock_var(mover),
kernel=math_model.functions[mover.kernel]
)
elif 'argmax' in mover.body:
# Bellman optimization mover
return BellmanMaxOperator(
action_space=math_model.spaces[mover.action_space],
constraint_set=math_model.constraints[mover.constraint],
objective=math_model.functions[mover.q_kernel]
)
# ... similar for forward movers
Purpose of Stage 3¶
This stage is conceptual/theoretical: - Provides the mathematical interpretation for human understanding - Enables theoretical analysis (contraction properties, convergence) - Not required for execution — you can skip to stage 4 if you only want numerical results
7.5 Stage 4: Methodization (M)¶
Three-File Architecture¶
Methodization reads from three sources:
| File | Purpose |
|---|---|
stage.yml |
Operator declarations (operators: block) |
operation-registry.yml |
Available schemes/methods |
methodization.yml |
Model-specific choices |
def methodize_stage(validated_stage, registry, method_config):
"""
Apply methodization functor M.
Reads:
- validated_stage.operators (what operators are used)
- registry (what schemes exist)
- method_config (which choices for this model)
This happens BEFORE calibration.
The output has schemes chosen but settings still symbolic (settings_profile labels).
"""
methodized = MethodizedStage(validated_stage)
# 1. Methodize logical operators (E_w, argmax, etc.)
for op_config in method_config.logical_operators:
op = validated_stage.find_operator(op_config.symbol)
scheme = registry.lookup_scheme(op_config.scheme)
method = registry.lookup_method(scheme, op_config.method)
methodized.operator_schemes[op_config.instance] = {
'scheme': scheme,
'method': method,
'settings_profile': op_config.settings_profile
}
# 2. Methodize movers (attach APPROX to outputs)
for mover_config in method_config.movers:
mover = validated_stage.find_mover(mover_config.mover)
methodized_mover = methodize_mover(mover, mover_config, registry)
methodized.movers[mover.name] = methodized_mover
return methodized
Methodizing a Mover¶
def methodize_mover(mover, mover_config, registry):
"""
Attach schemes to a mover from external config.
Key change: methods come from methodization.yml, NOT from @methods in perches.
"""
methodized = MethodizedMover(mover)
# Mover-level scheme/method
methodized.scheme = mover_config.scheme
methodized.method = mover_config.method
# Attach APPROX to each output function
for approx_config in mover_config.approx:
methodized.approx_schemes[approx_config.target] = {
'scheme': approx_config.scheme,
'method': approx_config.method,
'settings_profile': approx_config.settings_profile,
'acts_on_space': approx_config.acts_on_space
}
return methodized
Method Configuration File¶
Methods are configured in a separate file (not in perches):
# consind-methodization.yml
stage: consind
methodization:
logical_operators:
- symbol: E_{}
instance: E_w
scheme: shock_expectation
method: !gauss-hermite
settings_profile: expect_w_settings
attaches_to:
mover: dcsn_to_arvl
- symbol: argmax_{}
instance: argmax_a
scheme: statewise_argmax
method: !golden-section
settings_profile: argmax_a_settings
movers:
- mover: dcsn_to_arvl
scheme: bellman_backward
method: !generic
approx:
- target: Va
scheme: interpolation
method: !linear
settings_profile: Va_interp_settings
acts_on_space: Xa
Authoring note (optional): flat bindings form
The same methodization information can be written without nesting as a flat list of bindings (grouping keys are presentation-only):
stage: consind
bindings:
- operator: E_w # from syntax `E_{w}(...)` (similarly: `E_{y}(...)` → `E_y`)
scheme: shock_expectation
method: !gauss-hermite
settings:
n_nodes: n_w_nodes # name in `symbols.settings`; numeric value in settings.yml
- operator: argmax_a
scheme: statewise_argmax
method: !golden-section
settings:
tol: argmax_tol
- operator: mover:dcsn_to_arvl
scheme: bellman_backward
method: !generic
- operator: approx:Va
scheme: interpolation
method: !linear
settings:
extrapolation: extrapolation_mode
acts_on_space: Xa
Key point: The stage YAML contains no @methods annotations. Movers are purely symbolic.
Kernels vs. methods (important):
- Methodization does not attach methods to arbitrary “equations” or “kernels” just because they exist.
- Methodization attaches schemes/methods to operator instances (e.g., E_w, argmax_a, draw ~, APPROX / interpolation) and to movers as computational units.
- In particular, many forward computations are push-forward operators derived from a transition kernel + a shock distribution. The transition kernel is a pure function object; the push-forward (simulation / quadrature / sampling) is what needs a method.
- A kernel only becomes method-relevant when you intend to evaluate it under ρ and its evaluation invokes methodized operators (e.g., it calls E_w[...], solves an argmax, evaluates an approximated function off-grid, draws from a distribution, etc.).
APPROX Insertion¶
Each mover's output functions are wrapped in APPROX:
From methodization config:
movers:
- mover: dcsn_to_arvl
approx:
- target: Va
scheme: interpolation
method: !linear
settings_profile: Va_interp_settings
After methodization:
B_arvl produces APPROX(Va, {
scheme: interpolation,
method: !linear,
settings: {extrapolation: extrapolation_mode} # values come from settings.yml
})
The settings_profile is just a label — concrete values come from calibration.
7.6 Stage 5: Calibration (C)¶
Applying Calibration¶
After methodization, calibration resolves settings_profile labels to concrete values:
def calibrate_stage(methodized_stage, calibration):
"""
Apply calibration functor C.
Requires: methodization already done.
Resolves: settings_profile labels from methodization config.
"""
calibrated = CalibratedStage(methodized_stage)
# 1. Apply parameter calibration (economic model)
for param, value in calibration.parameters.items():
calibrated.substitute(param, value)
# e.g., β → 0.96, γ → 2.0
# 2. Resolve method settings (numerical schemes)
for profile_name, settings in calibration.method_settings.items():
calibrated.resolve_settings_profile(profile_name, settings)
# e.g., expect_w_settings → {nodes: 9, multidim: 'tensor'}
# e.g., Va_interp_settings → {extrapolation: 'clip'}
# e.g., Xa_grid_settings → {n_points: 50, bounds: [0.1, 10.0]}
return calibrated
Calibration Config File¶
# consind-calibration.yml
stage: consind
# Economic parameters (define the math problem)
parameters:
β: 0.96
γ: 2.0
r: 1.04
μ_w: 0.0
σ_w: 0.1
# Method settings (resolve settings_profile labels)
method_settings:
expect_w_settings:
nodes: 9
multidim: tensor
argmax_a_settings:
tol: 1.0e-8
max_iter: 100
Va_interp_settings:
extrapolation: clip
Xa_grid_settings:
n_points: 50
bounds: [0.1, 10.0]
Two-Part Calibration¶
def load_calibration(calibration_file):
with open(calibration_file, 'r') as f:
calib = yaml.safe_load(f)
return Calibration(
parameters=calib.get('parameters', {}), # β, γ, r, ...
settings=calib.get('settings', {}) # n_xa, tol, ...
)
Validation After Calibration¶
def validate_calibration(calibrated_stage):
# Check all parameters have values
for param in calibrated_stage.all_parameters:
if not calibrated_stage.has_value(param):
raise CalibrationError(f"Missing parameter: {param}")
# Check all settings have values
for setting in calibrated_stage.all_settings:
if not calibrated_stage.has_value(setting):
raise CalibrationError(f"Missing setting: {setting}")
# Check value ranges
for param, value in calibrated_stage.param_values.items():
if not in_declared_range(param, value):
raise CalibrationError(f"Out of range: {param}={value}")
7.7 Stage 6: Representation (ρ)¶
Creating Computational Objects¶
The representation map ρ transforms calibrated objects into an executatable representation:
def represent_stage(calibrated_stage):
"""
Apply representation map ρ.
Requires: methodization AND calibration complete.
Produces: executable numerical objects.
"""
rho = RepresentationMap()
numerical_meaning = NumericalMeaningTable()
# Create grids for spaces
for space in calibrated_stage.spaces:
grid = rho.create_grid(space, calibrated_stage)
numerical_meaning.register(space.name, grid)
# Create implementations for operators
for mover in calibrated_stage.movers:
impl = rho.create_mover_implementation(mover, numerical_meaning)
numerical_meaning.register(mover.name, impl)
return ComputationalStage(numerical_meaning)
ρ for the Arrival Mover¶
End-to-end example for B_arvl:
def rho_arrival_mover(mover, numerical_meaning, calibration):
"""
ρ(B_arvl) with:
E_w: !gauss-hermite(n_nodes=5)
Va: !linear(grid_size=50)
"""
# Get calibrated values
n_nodes = calibration.settings['n_nodes'] # 5
n_xa = calibration.settings['n_xa'] # 50
mu_w = calibration.params['μ_w'] # 0.0
sigma_w = calibration.params['σ_w'] # 0.1
# Create quadrature for E_w
nodes, weights = gauss_hermite_quadrature(n_nodes, mu_w, sigma_w)
# Get grids
grid_xa = numerical_meaning.lookup_grid('Xa')
# Get transition kernel
g_av = numerical_meaning.lookup_function('g_av')
# Create the operator implementation
def arrival_backward(Vv_interpolator):
"""The actual numerical operator."""
Va_values = np.zeros(n_xa)
for i, xa in enumerate(grid_xa):
# Compute E_w[Vv(g_av(xa, w))] via quadrature
Va_values[i] = sum(
w * Vv_interpolator(g_av(xa, node))
for node, w in zip(nodes, weights)
)
# Return as interpolator (APPROX)
return LinearInterpolator(grid_xa, Va_values)
return arrival_backward
Conditional Expectation Details¶
The arrival mover implements a conditional expectation:
Key aspects of the ρ implementation: - Conditioning variable: \(x_{\prec}\) (arrival state) — we iterate over grid points - Integration variable: \(w\) (shock) — we sum over quadrature nodes - Kernel: \(\mathrm{g}_{\prec\sim}\) — transforms \((x_{\prec}, w)\) to \(x_{\sim}\)
The @methods annotation specifies:
- E_w: !gauss-hermite(n_nodes) — how to approximate the integral
- Va: !linear(grid_size) — how to store the result
7.8 Stage 7: Solving¶
Solver Initialization¶
def initialize_solver(computational_stage, settings):
solver = ADCSolver(
arrival_op=computational_stage.get_operator('B_arvl'),
decision_op=computational_stage.get_operator('B_dcsn'),
numerical_meaning=computational_stage.numerical_meaning,
tolerance=settings['tol'],
max_iterations=settings['max_iter']
)
# Set initial guess
solver.set_initial_guess(create_initial_guess(computational_stage))
return solver
Value Function Iteration¶
def solve_value_iteration(solver):
converged = False
iteration = 0
while not converged and iteration < solver.max_iterations:
# Store old value function
Ve_old = solver.continuation_value.copy()
# Backward pass through movers
# B_dcsn: Ve → (Vv, astar)
Vv, astar = solver.decision_op(solver.continuation_value)
# B_arvl: Vv → Va
Va = solver.arrival_op(Vv)
# Link to next period (often identity)
Ve_new = Va
# Check convergence
error = compute_error(Ve_new, Ve_old)
converged = error < solver.tolerance
# Update
solver.update(Va, Vv, Ve_new, astar)
iteration += 1
logger.info(f"Iteration {iteration}: error = {error:.6e}")
return Solution(Va, Vv, Ve_new, astar, converged, iteration)
7.9 Stage 8: Analysis and Export¶
Computing Statistics¶
def compute_statistics(solution, simulations):
return {
'policy_function': solution.astar.to_dataframe(),
'value_function': solution.Va.to_dataframe(),
'convergence_achieved': solution.converged,
'iterations_required': solution.iterations,
'mean_consumption': np.mean(simulations.consumption),
'wealth_distribution': compute_distribution(simulations.wealth),
}
7.10 Complete Pipeline Summary¶
The Full Transformation Chain¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ DDSL EXECUTION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ YAML Files │
│ │ │
│ ▼ [1. Parse] │
│ Syntactic Stage S │
│ │ │
│ ▼ [2. Validate] │
│ Validated Stage │
│ │ │
│ ├────────────────────► [3. Υ] ────► Mathematical Model │
│ │ (for theoretical analysis) │
│ │ │
│ ▼ [4. Methodize] M │
│ Methodized Stage (S, M) │
│ │ • APPROX attached to movers │
│ │ • Schemes chosen for E_w, argmax, ~ │
│ │ • Settings still symbolic (n_xa, n_nodes, tol) │
│ │ │
│ ▼ [5. Calibrate] C │
│ Calibrated Stage (S, M, C) │
│ │ • Parameters have values (β=0.96, γ=2.0) │
│ │ • Settings have values (n_xa=50, n_nodes=5) │
│ │ │
│ ▼ [6. Represent] ρ │
│ Computational Stage │
│ │ • Grids created (numpy arrays) │
│ │ • Operators implemented (callables) │
│ │ • Numerical meaning table populated │
│ │ │
│ ▼ [7. Solve] │
│ Numerical Solution │
│ │ • Value functions (interpolators) │
│ │ • Policy functions (interpolators) │
│ │ │
│ ▼ [8. Analyze] │
│ Economic Insights │
│ • Statistics, plots, exports │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Threading Υ and ρ¶
| Stage | Which Map? | What It Does |
|---|---|---|
| 3 | Υ | Syntax → Mathematical operators (true, Platonic) |
| 6 | ρ | Calibrated syntax → Numerical operators (approximate) |
Key insight: - Υ can be applied early (after validation, possibly with parameters) - ρ requires methodization AND calibration (stages 4-5 must complete first) - The numerical operators under ρ approximate the mathematical operators under Υ
End-to-End Example: Buffer-Stock B_arvl¶
1. PARSE
B_arvl: Vv -> Va @via |
xv = g_av(xa, w)
Va(xa) = E_w[Vv(xv)]
@methods: {E_w: expect_w, Va: interp_Va}
2. VALIDATE
✓ B_arvl is a valid mover declaration
✓ E_w is a registered expectation operator
✓ expect_w and interp_Va are declared methods
3. Υ (MATHEMATICAL MEANING)
Υ(B_arvl) = conditional expectation operator
(B_arvl V)(xa) = ∫ V(g_av(xa, w)) dμ_w(w)
4. M (METHODIZE)
M(B_arvl) = (B_arvl, {
E_w: !gauss-hermite(n_nodes),
Va: APPROX(!linear(space: Xa, grid_size: n_xa))
})
5. C (CALIBRATE)
C(M(B_arvl)) = (B_arvl, {
E_w: !gauss-hermite(n_nodes=5),
Va: APPROX(!linear(space: Xa, grid_size=50))
})
with μ_w = 0.0, σ_w = 0.1
6. ρ (REPRESENT)
ρ(C(M(B_arvl))) = Python function:
def arrival_backward(Vv_interp):
nodes, weights = gauss_hermite(5, 0.0, 0.1)
Va_values = zeros(50)
for i, xa in enumerate(linspace(0.1, 10.0, 50)):
Va_values[i] = sum(w * Vv_interp(xa * r + node)
for node, w in zip(nodes, weights))
return LinearInterpolator(grid_xa, Va_values)
7. SOLVE
Apply arrival_backward iteratively until convergence
8. ANALYZE
Extract policy, value functions, statistics
7.11 Summary¶
The execution pipeline provides:
- Clear staging: Eight distinct transformation stages
- Correct ordering: M (methodization) before C (calibration)
- Dual semantics: Υ for mathematical meaning, ρ for computational
- Explicit approximation: APPROX operators attached during methodization
- Operator-driven: Registry operators get implementations through ρ
- Complete traceability: From YAML to numerical result
The pipeline ensures: - Separation of concerns: Syntax, math, algorithms, numbers are distinct - Reproducibility: Same inputs → same outputs - Flexibility: Can change methods without changing math, change settings without changing methods - Correctness: Numerical solutions approximate mathematical solutions