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
Submodelbase 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: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.- ✅ Correct
- ❌ Wrong
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.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: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
- Shallow Extraction: Parser extracts direct variable references from expressions
- Function Arguments: All arguments to
calc_*functions are extracted as dependencies - 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 noscenarios 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:
- Intervention gating - if
current_time < intervention_start_timeandkeyis a scenario override, return calibration value (or model default) - Dynamic parameters - if the value is a
__dynamic__spec, resolve it to a scalar atcurrent_time - Plain scalar - return the value from
all_paramsdirectly
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
sin, cos, tan, exp, log, sqrt, abs, min, max, pow, floor, ceil, pi, e. The variable t represents current_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:
__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:
.) 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.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:
- Baseline modification: Applied directly to the baseline run as regular parameter overrides
- Pre-intervention defaults: For scenario runs with intervention gating, calibration values are used as the pre-intervention value (taking priority over model defaults)

