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
'budget.s_government_budget': { 'name': 'Government Budget', # Display name 'units': 'dollars', # Unit of measurement 'description': 'Accumulated fund', # Plain text description 'default': 100000.0, # Initial value (THIS IS THE INITIAL VALUE) 'min': -1000000.0, # Minimum allowed 'max': 10000000.0, # Maximum allowed 'type': 'slider', # UI control type 'category': 'stock' # MUST be 'stock'}
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.
# Step 1: Define flow variablesgovernment_tax_revenue_flow = tax_revenuegovernment_spending_flow = spending# Step 2: Derivative DIRECTLY references flow variable namesd_government_budget_dt = government_tax_revenue_flow - government_spending_flow# Step 3: flows dict uses the SAME variable namesflows = { 'budget.government_tax_revenue_flow': government_tax_revenue_flow, # ✅ Same name 'budget.government_spending_flow': government_spending_flow # ✅ Same name}
Common Mistakes:
Using different variable names in derivative vs flows dict
Using intermediate “net” variables that hide individual flows
For complex intermediate calculations (conditionals, aggregations, multi-step computations), define module-level calculation functions where ALL dependencies are explicit in the function signature.
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.
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":
# These two calls are equivalent:model.run_multi_scenario({"retail.order_quantity": 800})model.run_multi_scenario({"scenarios": [{"id": "current", "params": {"retail.order_quantity": 800}}]})
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.
# Single run (backward compatible)run_interactive_simulation(model, {"retail.order_quantity": 800})# Multi-scenario (auto-detected)run_interactive_simulation(model, { "include_baseline": True, "scenarios": [{"id": "s1", "params": {"retail.order_quantity": 800}}]})
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.
Intervention gating - if current_time < intervention_start_time and key is a scenario override, return calibration value (or model default)
Dynamic parameters - if the value is a __dynamic__ spec, resolve it to a scalar at current_time
Plain scalar - return the value from all_params directly
# SimulationContext is constructed with gating context automatically# by the orchestrator during run_multi_scenario(). Submodels don't need# to do anything special — sim_context.get_param() handles it.# Before intervention_start_time:sim_context.get_param('retail.order_quantity') # → calibration value or default# After intervention_start_time:sim_context.get_param('retail.order_quantity') # → scenario override value
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.
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().
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.
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
Meta-keys are special config keys that control execution behavior. They are stripped from the parameter dict before the ODE solver sees them.
Key
Type
Default
Description
include_baseline
bool
false
Run a pure-defaults baseline alongside scenarios
scenarios
list
[]
List of {id, params} scenario definitions
__calibration_params__
dict
{}
Parameter overrides applied to baseline and used as pre-intervention defaults
__initial_state__
dict
{}
Stock value overrides at t=0 (warm-start / game-mode)
__time_window__
dict
null
{start, end} to filter output time range
__output_format__
string
"full"
"full", "summary", "stacked", or "all"
__summary_stats__
list
default set
Which statistics to compute in summary mode
__summary_variable__
string
first stock
Which stock to summarize
Meta-keys must never collide with actual model parameter names. They are defined in META_KEYS frozenset and automatically excluded from _clean_params().
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:
state = {} # Start with defaultsstep_size = 1 # 1 week per turnfor week in range(52): result = model.run_simulation({ 'simulation.duration': step_size, '__initial_state__': state, # Player can change params each turn 'retail.order_quantity': player_decision, }) # Capture final stock values as next initial state state = { stock: values[-1] for stock, values in result['stocks'].items() }
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_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)
result = model.run_multi_scenario({ 'include_baseline': True, 'simulation.duration': 104, 'simulation.intervention_start_time': 52, '__calibration_params__': { 'retail.order_quantity': 600, # Calibrated to real-world data }, 'scenarios': [ {'id': 'high', 'params': {'retail.order_quantity': 1000}}, ]})# baseline: order_quantity = 600 for the entire run# high scenario:# t < 52: order_quantity = 600 (calibration value, via intervention gating)# t >= 52: order_quantity = 1000 (scenario override kicks in)