Skip to main content

Overview

The Veydra Model Standard (VMS) is a set of conventions for building system dynamics models that are both executable and analyzable. VMS-compliant models enable automatic generation of stock-and-flow diagrams, causal loop diagrams, and other analytical visualizations through AST (Abstract Syntax Tree) parsing.

Executable

Models run in Python with numerical simulation via SciPy

Analyzable

AST parsing extracts structure for automatic diagram generation

Modular

Independent submodels can be composed into larger systems

Traceable

Explicit dependencies enable causal loop analysis

Core Architecture

VMS models consist of:
  • Submodels: Independent modules that inherit from Submodel base class
  • VARIABLES Dictionary: Module-level declaration of all model variables
  • SimulationContext: Runtime context for accessing stocks, parameters, and intervention gating
  • Calculation Functions: Module-level functions for complex computations
  • Multi-Scenario Execution: run_multi_scenario() runs baseline + N scenarios in a single call
  • Output Helpers: Summary tables, stacked tables, and time-window filtering

Submodel Structure

Every submodel follows this structure:
Required Rules:
  • Do NOT override __init__ method
  • Do NOT override get_variables() method
  • VARIABLES dict MUST be at module level
  • Only implement calculate_derivatives_and_flows()

VARIABLES Dictionary

The VARIABLES dictionary declares all model variables at module level.

Stock Definition

Parameter Definition

Naming Conventions

Critical Rule: The default field in a stock definition IS the initial value. Never create separate initial_* parameters.

Stock-and-Flow Modeling

Separate Flows by Direction

Each parameter affecting a stock must have its own distinct flow. Never collapse flows.

Why This Matters

  • Individual flows enable proper stock-and-flow diagram generation
  • Collapsed flows obscure causal relationships
  • AST parser cannot trace through intermediate “net” variables

Direct Flow Reference Rule

Derivatives MUST directly reference the same variable names that appear in the flows dictionary. This enables the AST parser to trace stock-flow relationships for automatic diagram generation.
Common Mistakes:
  • Using different variable names in derivative vs flows dict
  • Using intermediate “net” variables that hide individual flows

Calculation Function Convention

For complex intermediate calculations (conditionals, aggregations, multi-step computations), define module-level calculation functions where ALL dependencies are explicit in the function signature.

Why Use Calculation Functions

The AST parser extracts all function call arguments as dependencies. This makes complex logic fully traceable.

Pattern

Function Naming Convention

  • Use calc_ prefix: calc_pool_frac_ill, calc_total_income, calc_tax_revenue
  • Name should describe what is being calculated
  • Keep signatures to 6 or fewer parameters when possible

When to Use Calculation Functions

Derivative Patterns

Dictionary Format (Required)

Always return derivatives as a dictionary mapping stock names to their rates of change:
Never use array format - it’s unclear which stock is which:

Derivative Expression Pattern

Build derivatives from individual flows with explicit addition/subtraction:

Accessing Stocks and Parameters

AST Traceability

The VMS conventions enable automatic AST parsing for:
1

Stock-Flow Diagram Generation

Automatically creates visual diagrams showing stocks, flows, and their connections
2

Causal Loop Analysis

Traces parameter influences through the model to identify feedback loops
3

Dependency Extraction

Identifies all variable dependencies for each calculation

How AST Parsing Works

  1. Shallow Extraction: Parser extracts direct variable references from expressions
  2. Function Arguments: All arguments to calc_* functions are extracted as dependencies
  3. Flow Dictionary Keys: Flow names from the flows dict are matched to derivative expressions

Complete Example

Here’s a complete VMS-compliant submodel:

Multi-Scenario Execution

VeydraModelStandard.run_multi_scenario() runs one or many scenarios in a single call with shared simulation parameters and calibration.

Input Config Shape

How It Works

1

Extract meta-keys

include_baseline, __calibration_params__, __initial_state__, __time_window__, __output_format__, __summary_stats__, __summary_variable__, and scenarios are pulled out of the config before parameter resolution. They never reach the ODE solver.
2

Run baseline (if requested)

When include_baseline is true, a run is executed with only the shared simulation params plus calibration values. No intervention gating is applied (there are no scenario overrides to gate).
3

Run each scenario

For every entry in scenarios, the simulation params are merged with that scenario’s params. If simulation.intervention_start_time is set, the scenario override keys are gated so they only take effect after that time (see Intervention Gating below).
4

Format output

Results are assembled in the requested __output_format__ and returned.

Implicit Current Scenario

If no scenarios list is provided but there are non-simulation override keys in the config, they are automatically bundled into a single scenario with id "current":

Auto-Detection in run_interactive_simulation

run_interactive_simulation() automatically routes to run_multi_scenario() when any of these keys are present: scenarios, include_baseline, or __output_format__. Otherwise it falls through to a plain run_simulation() call for full backward compatibility.

Intervention Gating

SimulationContext.get_param() natively supports intervention gating. When a scenario is run with simulation.intervention_start_time set, scenario override parameters return their default (or calibration) value before the intervention time, and the scenario value after it. This eliminates the need for any external monkey-patching or runtime code injection.

Priority Order

get_param(key) resolves values in this order:
  1. Intervention gating - if current_time < intervention_start_time and key is a scenario override, return calibration value (or model default)
  2. Dynamic parameters - if the value is a __dynamic__ spec, resolve it to a scalar at current_time
  3. Plain scalar - return the value from all_params directly

SimulationContext Constructor

Submodel code does not change at all. The gating is handled entirely inside get_param(), so existing sim_context.get_param('retail.order_quantity') calls work with or without gating enabled.

Dynamic (Time-Varying) Parameters

Parameters can be time-varying instead of scalar. Dynamic parameters are dicts with __dynamic__: true and are resolved to a scalar at current_time by SimulationContext.get_param().

Modes

Example

Available math functions in expressions: sin, cos, tan, exp, log, sqrt, abs, min, max, pow, floor, ceil, pi, e. The variable t represents current_time.
Dynamic parameter specs are preserved through _clean_params() and _resolve_parameters() — they bypass scalar validation (min/max clamping) since their value changes over time.

Output Formats

The __output_format__ meta-key controls how run_multi_scenario() returns results.

"full" (default)

Returns the complete time-series for every scenario:

"summary"

Returns a compact table with one row per scenario and configurable summary statistics:
Customize with __summary_stats__ (list of stat names) and __summary_variable__ (which stock to summarize). Available stats: final_value, peak, min, mean, growth_pct, variance, time_to_peak

"stacked"

Returns a long-format table suitable for charting in Google Sheets:
Variable names use shorthand (last segment after .) for readability.

"all"

Returns all three formats in a single response: scenarios, summary, and stacked.

Meta-Keys Reference

Meta-keys are special config keys that control execution behavior. They are stripped from the parameter dict before the ODE solver sees them.
Meta-keys must never collide with actual model parameter names. They are defined in META_KEYS frozenset and automatically excluded from _clean_params().

Initial State and Game-Mode Stepping

The __initial_state__ meta-key overrides stock values at t=0, enabling two use cases:

Warm-Start

Resume a simulation from a previously saved state:

Game-Mode Stepping

Run the simulation one step at a time (e.g., one week per turn). Each step feeds the final state of the previous step as __initial_state__ for the next:
In the frontend, game-mode stepping runs client-side via Pyodide (not server-side). The __initial_state__ dict is passed in each step call.

Calibration Parameters

__calibration_params__ are shared across all scenarios in a run_multi_scenario() call. They serve two purposes:
  1. Baseline modification: Applied directly to the baseline run as regular parameter overrides
  2. Pre-intervention defaults: For scenario runs with intervention gating, calibration values are used as the pre-intervention value (taking priority over model defaults)

Helper Functions

compute_summary_stats

Compute named statistics for a single time-series:

build_summary_table

One row per scenario with configurable stats:

build_stacked_table

Long-format table for all scenarios (suitable for Google Sheets charting):

apply_time_window

Filter a single-run result dict to a time range:

Quick Reference Checklist

When writing VMS-compliant models, verify: