Skip to content

Backend Implementation Guide

This document provides guidance on implementing new backends for the computational framework.

Core Interface Requirements

Every backend must implement these core interfaces through its whisperer:

  1. Terminal Condition Population:

    def populate_cntn_terminal(self, perch_cntn, config=None):
        """Populate terminal condition for continuation perch"""
    

  2. Continuation Value Population:

    def populate_cntn_curr_from_arvl_next(self, perch_cntn, stage_curr, stage_next):
        """Populate continuation value from next stage's arrival perch"""
    

  3. Stage Solution:

    def solve_stage(self, stage):
        """Solve a stage by populating all perch instances with computational content"""
    

  4. Stage Simulation:

    def simulate_stage(self, stage, initial_distribution):
        """Simulate a stage with the given initial distribution"""
    

CompObject Interface

Every backend must package its computational functions in a CompObject that exposes:

  1. Value Function: value_function(state) method
  2. Decision Rule: dcsn_rule(state) method

How these functions are implemented internally is entirely up to the backend.

Registration Process

To make a backend available to the framework:

from bellman.whisperers.registry import BackendRegistry

# Register your backend
BackendRegistry.register(
    "my_backend",
    MyBackendWhisperer,
    "Description of my specialized backend"
)

After registration, users can select your backend like any other:

whisperer = WhispererFactory.create("my_backend")

Configuration Management

Your backend should use the configuration management system:

from bellman.config import config

# Access backend-specific configuration
backend_config_path = config.backend_path("my_backend", "model_name", file_type="config")

Backend Implementation Strategies

1. Adapting Existing Computational Libraries

When wrapping an existing computational library:

class ExistingLibWhisperer(BaseWhisperer):
    def __init__(self, **kwargs):
        try:
            import existing_lib
            self.lib = existing_lib
        except ImportError:
            raise ImportError("existing_lib is required for this backend")

    def solve_stage(self, stage):
        # Translate stage to existing_lib format
        lib_problem = self._translate_to_lib_format(stage)
        # Solve using the library
        lib_solution = self.lib.solve(lib_problem)
        # Translate solution back to framework format
        self._update_stage_with_solution(stage, lib_solution)

2. Building a New Computational Engine

When building a new computational engine:

class NewEngineWhisperer(BaseWhisperer):
    def __init__(self, **kwargs):
        from .my_engine import MyEngine
        self.engine = MyEngine(**kwargs)

    def solve_stage(self, stage):
        # Use your custom engine
        solution = self.engine.solve(stage.params, stage.methods)
        # Update stage with solution
        self._update_stage_with_solution(stage, solution)

3. Lazily Loading Dependencies

To avoid unnecessary dependencies:

class OptionalDependencyWhisperer(BaseWhisperer):
    def __init__(self, **kwargs):
        self.engine = None

    def _ensure_engine(self):
        if self.engine is None:
            try:
                from .expensive_import import ExpensiveEngine
                self.engine = ExpensiveEngine()
            except ImportError:
                raise ImportError("ExpensiveEngine is required for this operation")

    def solve_stage(self, stage):
        self._ensure_engine()
        # Now use the engine
        self.engine.solve(stage)

Example Backends

See specific backend implementation guides: - Dolo Backend Implementation - HARK Backend Implementation