Skip to content

YAML to Model Conversion

Overview

The conversion from YAML file to Model object happens through yaml_import() and involves multiple parsing stages.

The YAML Structure

Standard YAML Model File

symbols:
    exogenous: [y]
    states: [w]
    controls: [c]
    expectations: [mr]
    poststates: [a]
    parameters: [β, γ, σ, ρ, r]

equations:
    transition:
        - w[t] = exp(y[t]) + (w[t-1]-c[t-1])*r

    arbitrage:
        - β*(c[t+1]/c[t])^(-γ)*r - 1 | 0.0<=c[t]<=w[t]

    # New-style block notation
    auxiliary_direct_egm: |
        a[t] = w[t] - c[t]

    expectation: |
        mr[t] = β*(c[t+1])^(-γ)*r

calibration:
    β: 0.96
    γ: 4.0
    σ: 0.1
    ρ: 0.0
    r: 1.00
    w: 1.0
    c: 0.9*w  # Can use expressions!

domain:
    w: [0.01, 4.0]

exogenous: !UNormal
    sigma: σ

options:
    grid: !Cartesian
        orders: [100]
        bounds: [[0.0, 4.0]]

Conversion Process

Step 1: YAML Import (model_import.py)

def yaml_import(fname, check=True):
    # 1. Read file or URL
    txt = read_file_or_url(fname)

    # 2. Parse YAML
    data = yaml.compose(txt)

    # 3. Add filename for reference
    data["filename"] = fname

    # 4. Create Model object
    return Model(data, check=check)

Step 2: Model Initialization (model.py)

class Model:
    def __init__(self, data):
        self.data = data  # Raw YAML data

        # Initialize lazy properties
        self.__symbols__ = None
        self.__equations__ = None
        self.__calibration__ = None
        self.__functions__ = None

Step 3: Symbol Extraction

When model.symbols is first accessed:

@property
def symbols(self):
    if self.__symbols__ is None:
        symbols = LoosyDict()

        # Extract from YAML
        for category in self.data["symbols"]:
            symbols[category] = self.data["symbols"][category]

        self.__symbols__ = symbols

    return self.__symbols__

Result:

{
    'exogenous': ['y'],
    'states': ['w'],
    'controls': ['c'],
    'parameters': ['β', 'γ', 'σ', 'ρ', 'r']
}

Step 4: Equation Parsing

When model.equations is accessed:

@property
def equations(self):
    if self.__equations__ is None:
        d = dict()

        for eq_name, eq_content in self.data["equations"].items():
            # Handle different equation styles
            if isinstance(eq_content, yaml.nodes.ScalarNode):
                # New style: equation_name: |
                #     equation content
                eqs = parse_string(eq_content, start="assignment_block")
            else:
                # Old style: equation_name:
                #     - equation1
                #     - equation2
                eq_list = []
                for eq_string in eq_content:
                    eq = parse_string(eq_string)
                    eq_list.append(eq)
                eqs = eq_list

            d[eq_name] = eqs

        self.__equations__ = d

    return self.__equations__

Step 5: Special Parsing Cases

Complementarity Conditions

Equations with bounds (arbitrage):

arbitrage:
    - β*(c[t+1]/c[t])^(-γ)*r - 1 | 0.0<=c[t]<=w[t]

Gets parsed into: - Main equation: β*(c[t+1]/c[t])^(-γ)*r - 1 - Lower bound: 0.0 - Upper bound: w[t]

Creates three functions: - arbitrage - The main equation - arbitrage_lb - Lower bound function - arbitrage_ub - Upper bound function

Multi-line Blocks

Using | for cleaner syntax:

transition: |
    w[t] = exp(y[t]) + a[t-1]*r
    # Can span multiple lines

Step 6: Calibration Processing

@property
def calibration(self):
    if self.__calibration__ is None:
        from dolo.compiler.misc import CalibrationDict

        # Create special calibration dictionary
        calib = CalibrationDict()

        for key, value in self.data["calibration"].items():
            if isinstance(value, str):
                # Parse expressions like "0.9*w"
                calib[key] = eval_expression(value, calib)
            else:
                calib[key] = value

        self.__calibration__ = calib

    return self.__calibration__

Step 7: Exogenous Process Creation

# YAML tag handlers create appropriate process objects
exogenous: !UNormal
    sigma: σ

Becomes:

model.exogenous = UNormal(sigma=model.calibration['σ'])

Other process types: - !VAR1 - AR(1) process - !MarkovChain - Discrete Markov chain - !Normal - IID normal - !MvNormal - Multivariate normal

Parsing Pipeline Details

Symbol Timing Notation

The parser recognizes time indices: - x[t] - Current period - x[t-1] - Previous period - x[t+1] - Next period

Variable Categories

Each variable must be declared in exactly one category:

symbols:
    states: [k, h]      # ✓ OK
    controls: [c, l]    # ✓ OK
    states: [c]         # ✗ Error: c already in controls

Expression Evaluation

The parser handles: - Standard math: +, -, *, /, ^ - Functions: exp(), log(), sqrt() - Greek letters: β, γ, σ - Time subscripts: [t], [t-1], [t+1]

Validation

During parsing, dolo checks: 1. All variables are declared in symbols 2. Time indices are consistent 3. Parameter references exist in calibration 4. Equation syntax is valid

Common Patterns

Multiple State Variables

symbols:
    states: [k, h, z]  # capital, human capital, productivity

equations:
    transition:
        - k[t] = (1-δ)*k[t-1] + i[t-1]
        - h[t] = h[t-1] + l[t-1]*η
        - z[t] = ρ*z[t-1] + eps[t]

Definitions (Auxiliary Variables)

definitions:
    y: exp(z)*k^α*h^(1-α)  # output

equations:
    transition:
        - k[t] = y[t] - c[t]  # Can use y

Conditional Equations

arbitrage:
    - u'(c[t]) = β*u'(c[t+1])*R[t+1] | c[t]>=0  # With constraint

Error Handling

Common parsing errors:

# Undefined variable
"w[t] = y[t] + k[t]"  # Error if k not in symbols

# Wrong timing
"w[t] = w[t+2]"  # Error: t+2 not supported

# Syntax error
"w[t] = exp(y[t]"  # Error: missing closing parenthesis

Next: Function Compilation →