Provide a JSON format for defining system models with optional simulation capabilities.
Supported model types include:
- Causal Loop Diagrams
- Stock and Flow Diagrams (System Dynamics and Differential Equation models)
- State and Transition Diagrams
The key goals of the format are:
- Minimal and simple
- Usable for diverse applications
- Reasonably readable and hand-editable
- Suitable for generation by LLMs
- Standardize formula syntax or functions across modeling applications.
Applications have varied capabilities, and aligning those is not an aim of this format. - Serve as the primary save file format for a modeling application.
This format is designed to be minimal. Applications may provide import/export support for ModelJSON, but the format does not seek to cover all the features of any given modeling application.
The root of the schema is a JSON object with the following properties:
name{string} [optional] – A string that provides the name of the model.description{string} [optional] – A description of the model.file_notes{string} [optional] - Notes on any issues that were encountered generating the ModelJSON file. This can be used to flag incompatibility issues or things that were stripped during export. It may be displayed to users on import.engine{"SIMULATION_PACKAGE"} [optional] – A string indicating a specific dialect of ModelJSON that is being used. This determines the syntax and behavior for formulas and units. See the Engines section below for more details.engine_settings{object} [optional] – Configuration settings for the selected engine.simulation{SimulationObject} [optional] – An object specifying the settings and parameters of the simulation.elements{ElementObject[]} [required] – An array of objects, each representing a basic element in the simulation (e.g., stocks, flows, variables, or links).visualizations{VisualizationObject[]} [optional] – Array of visualizations for simulation results.
algorithm{"RK1"|"RK4"} [optional] – Numerical algorithm for the simulation. "RK1" denotes Euler's method, and "RK4" a fourth-order Runge-Kutta.time_start{number} [optional] – The start time of the simulation.time_length{number} [optional] – The total length of the simulation.time_step{number} [optional] – The time step of the simulation.time_units{"SECONDS"|"MINUTES"|"HOURS"|"DAYS"|"WEEKS"|"MONTHS"|"YEARS"} [optional] – The time unitstime_start,time_lengthandtime_stepare defined in these units.
type{"STOCK"|"FLOW"|"VARIABLE"|"LINK"|"CONVERTER"|"STATE"|"TRANSITION"} [required] – The type of element.name{string} [optional for typeLINK, required otherwise] – A name for the element. Names are case-insensitive.description{string} [optional] – Additional descriptive text about the element.displayinteractive{boolean} [optional] – Iftrue, indicates that it should be simple for a user to adjust this element’s value (e.g., with an interactive slider).symbol{string} [optional] - An emoji used to visually represent the element.
Depending on the type, other properties may be required or optional. See Element Types below.
type{"TIME_SERIES"|"TABLE"} [required] - The type of the visualization.name{string} [optional] – A name for the visualization (may be displayed as a graph/table title).elements{string[]} [required] - An array of element names to include in the visualization.
Stocks are state variables that store a quantity (of water, money, individuals, etc...).
The following additional properties are available:
behaviorinitial_value{number|string} [optional] – The initial value of the stock.non_negative{boolean} [optional] – Iftrue, the stock cannot become negative. Defaults tofalse.units{string} [optional] – The units of the stock's value.
Additionally, the properties from the node and interactive-scalar mixins are included.
Flows move quantities between stocks.
The following additional properties are available:
behaviorvalue{string|number} [optional] – A formula or numeric rate for the flow.non_negative{boolean} [optional] – Iftrue, any negative flow value is set to 0. Defaults tofalse.units{string} [optional] – The units of the flow's value.
Note that Flows can only connect to and from stock elements.
Additionally, the properties from the connector and interactive-scalar mixins are included.
Variables are constants or formulas that are recalculated. In Causal Loop Diagrams, Variables should generally be used as the nodes.
The following additional properties are available:
behaviorvalue{number|string} [optional] – A formula or numeric value for the variable's value.units{string} [optional] – The units for the variable's value.
Additionally, the properties from the node and interactive-scalar mixins are included.
Links connect elements together. Elements that are not connected in some way (via a link, transition or flow) should not directly influence each other.
The following additional properties are available:
behaviorpolarity{"POSITIVE"|"NEGATIVE"|"NEUTRAL"} [optional] – The polarity of the link.
Additionally, the properties from the connector mixin are included.
A Converter is a table of input/output values. The input to the converter is looked up in the table to determine the output.
The following additional properties are available:
behaviorinput{"TIME"|"ELEMENT"} [optional] – IfTIME, the input for the converter is the current time (in simulation time units). IfELEMENT, the input is the value of another element.input_element{string} [optional] – IfinputisELEMENT, the name of that element.interpolation{"NONE"|"LINEAR"} [optional] – How to interpolate between values.data{[number, number][]} [optional] – An array of[input, output]pairs.units{string} [optional] – The units for the converter's output.
Additionally, the properties from the node mixin are included.
A State is a boolean, true/false state variable.
The following additional properties are available:
behaviorinitial_value{string|boolean} [optional] – A formula indicating whether the state is initially active (true) or not (false).
Additionally, the properties from the node mixin are included.
Transitions move state elements between true/false states. They can be triggered stochastically, using an equation which causes the transition to trigger when it evaluates to true, or with a timeout.
The following additional properties are available:
behaviortrigger{"PROBABILITY"|"CONDITION"|"TIMEOUT"} [optional] – The trigger type for the transition.value{number|string} [optional] – The value or formula associated with the trigger. If thetriggerisTIMEOUT, this is the time in the simulation time units until the trigger. If thetriggerisCONDITION, this is a formula evaluated each time step which will trigger when it evaluates totrue. If thetriggeris"PROBABILITY", this is the probability of a trigger happening within a single simulation time unit.
Note that transitions can only connect to and from states.
Additionally, the properties from the connector and interactive-scalar mixins are included.
Some element types share common properties. The following mixins may be used with multiple elements. For example, the Flow, Link and Transition elements use the Connector mixin.
These additional properties apply to connector-type elements:
from{string|null} [required] – The name of the start element.nullis used to indicate the connector is not connected at the start.to{string|null} [required] – The name of the end element.nullis used to indicate the connector is not connected at the end.displayfrom_coordinates{[number, number]} [optional - only specified iffromis not connected] - [x,y] position for the end of the connector from the top left corner.to_coordinates{[number, number]} [optional - only specified iftois not connected] - [x,y] position for the end of the connector from the top left corner.
These additional properties apply to node-type elements:
displaycoordinates{[number, number]} [optional] – [x,y] position of the top left corner of the node from the top left corner of the canvas.size{[number, number]} [optional] – The [width,height] of the element in pixels.
For elements with a slider or numeric interactive UI:
displayinteractive_min{number} [optional] – Minimum user-selectable value.interactive_max{number} [optional] – Maximum user-selectable value.
ModelJSON does not attempt to standardize model formulas. Instead, an engine property is specified, indicating how formulas should be parsed and evaluated. Code to parse and evaluate formulas for each recognized engine is provided.
The engine governs the parsing and evaluation of the following properties: value, initial_value, and units. Some engines may support additional configuration via the engine_settings property.
To add support for additional engines to ModelJSON, please submit a PR.
Currently supported engines:
NPM simulation package's format. Code is available to parse and evaluate formulas in the linked repository.
engine_settings supports the following properties for the SIMULATION_PACKAGE engine:
units{UnitObject[]} [optional] - Array of custom units used in the model. This allows extending the built-in units with additional custom unit conversions.globals{string} [optional] - Code that is run at the start of the simulation to set up global functions or variables or configure other behaviors.
name{string} - The name of the unit.base{string} [optional] - Another unit these units may be converted to. Required iffactoris specified.factor{number} [optional] - Units ofname*factorequals units ofbase. Required ifbaseis specified.
For example, you could define the following unit Century which automatically converts to and from the built-in unit Year:
{
"name": "Century",
"base": "Year",
"factor": 100
}In some cases, you may wish to use the ModelJSON format but need a property that it does not currently support. Please open an issue to discuss extending the format.
You may also take advantage of the fact that standard properties and constant values will never start with an underscore (_). If you wish to add a custom property or constant value to your own ModelJSON objects, you may do so as long as you prepend it with an underscore (e.g., _my_custom_property or "_CONSTANT_VALUE").
The following examples illustrate the usage of various features of the ModelJSON format.
{
"engine": "SIMULATION_PACKAGE",
"name": "Damped pendulum",
"description": "A simple damped pendulum model. Uses common SI units mapping to SIMULATION_PACKAGE built-in units.",
"simulation": {
"algorithm": "RK4",
"time_start": 0,
"time_length": 10,
"time_step": 0.1,
"time_units": "SECONDS"
},
"engine_settings": {
"globals": "# Acceleration due to gravity\ng <- {9.81 m/s^2}",
"units": [
{
"name": "kg",
"base": "grams",
"factor": 1000
},
{
"name": "s",
"base": "seconds",
"factor": 1
},
{
"name": "m",
"base": "meters",
"factor": 1
},
{
"name": "rad",
"base": "radians",
"factor": 1
}
]
},
"elements": [
{
"type": "STOCK",
"name": "Angle",
"display": {
"coordinates": [
270,
70
],
"size": [
120,
40
]
},
"behavior": {
"initial_value": 0.2,
"units": "rad"
}
},
{
"type": "STOCK",
"name": "Angular Velocity",
"display": {
"coordinates": [
270,
180
],
"size": [
120,
40
]
},
"behavior": {
"initial_value": 0,
"units": "rad/s"
}
},
{
"type": "VARIABLE",
"name": "Mass",
"display": {
"interactive": true,
"interactive_min": 0.1,
"interactive_max": 10,
"coordinates": [
30,
280
],
"size": [
80,
40
]
},
"behavior": {
"value": 1,
"units": "kg"
}
},
{
"type": "VARIABLE",
"name": "Length",
"display": {
"interactive": true,
"interactive_min": 0.1,
"interactive_max": 10,
"coordinates": [
330,
280
],
"size": [
80,
40
]
},
"behavior": {
"value": 1,
"units": "m"
}
},
{
"type": "VARIABLE",
"name": "Damping Coefficient",
"display": {
"interactive": true,
"interactive_min": 0,
"interactive_max": 1,
"coordinates": [
70,
380
],
"size": [
150,
40
]
},
"behavior": {
"value": 0.2,
"units": "kg * m^2 / s"
}
},
{
"type": "FLOW",
"name": "Angle Rate",
"from": null,
"to": "Angle",
"display": {
"from_coordinates": [
110,
90
]
},
"behavior": {
"value": "[Angular Velocity]",
"units": "rad / s"
}
},
{
"type": "FLOW",
"name": "Angular Acceleration",
"from": null,
"to": "Angular Velocity",
"display": {
"from_coordinates": [
110,
200
]
},
"behavior": {
"value": "-([Damping Coefficient]/([Mass]*[Length]^2))*[Angular Velocity] - (g /[Length])*sin([Angle]) * {1 radian}",
"units": "rad / s^2"
}
},
{
"type": "LINK",
"from": "Angular Velocity",
"to": "Angle Rate"
},
{
"type": "LINK",
"from": "Angle",
"to": "Angular Acceleration"
},
{
"type": "LINK",
"from": "Mass",
"to": "Angular Acceleration"
},
{
"type": "LINK",
"from": "Length",
"to": "Angular Acceleration"
},
{
"type": "LINK",
"from": "Damping Coefficient",
"to": "Angular Acceleration"
}
],
"visualizations": [
{
"type": "TIME_SERIES",
"name": "Pendulum States",
"elements": [
"Angle",
"Angular Velocity"
]
}
]
}{
"engine": "SIMULATION_PACKAGE",
"name": "Population Growth",
"simulation": {
"algorithm": "RK4",
"time_start": 0,
"time_length": 20,
"time_step": 0.2,
"time_units": "YEARS"
},
"elements": [
{
"type": "STOCK",
"name": "Population",
"display": {
"coordinates": [130, 210],
"size": [100, 40]
},
"behavior": {
"initial_value": 1,
"non_negative": true
}
},
{
"type": "CONVERTER",
"name": "Growth Rate",
"display": {
"coordinates": [260, 90],
"size": [120, 50]
},
"behavior": {
"input": "ELEMENT",
"interpolation": "LINEAR",
"data": [
[0, 2],
[1500, 1.07],
[3990, 0.429],
[6780, 0.125],
[10000, 0]
],
"input_element": "Population"
}
},
{
"type": "FLOW",
"name": "Flow",
"from": null,
"to": "Population",
"display": {
"from_coordinates": [180, 60]
},
"behavior": {
"value": "[Population] * [Growth Rate]",
"non_negative": true
}
},
{
"type": "LINK",
"from": "Growth Rate",
"to": "Flow"
},
{
"type": "LINK",
"from": "Population",
"to": "Growth Rate"
}
],
"visualizations": [
{
"type": "TIME_SERIES",
"name": "Default Display",
"elements": [
"Population"
]
}
]
}{
"engine": "SIMULATION_PACKAGE",
"name": "Predator Prey Model",
"description": "Version of the classic Lotka-Volterra model.",
"simulation": {
"algorithm": "RK4",
"time_start": 2000,
"time_length": 50,
"time_step": 0.5,
"time_units": "YEARS"
},
"elements": [
{
"type": "STOCK",
"name": "Prey",
"behavior": {
"initial_value": 400,
"non_negative": true,
"units": "Prey"
},
"display": {
"symbol": "🐰",
"interactive": true,
"interactive_min": 0,
"interactive_max": 1000,
"coordinates": [135, 214],
"size": [100, 40]
}
},
{
"type": "VARIABLE",
"name": "Prey Birth Rate",
"display": {
"coordinates": [30, 30],
"size": [120, 50]
},
"behavior": {
"value": 0.25,
"units": "1 / Years"
}
},
{
"type": "VARIABLE",
"name": "Prey Death Rate",
"display": {
"coordinates": [285, 371],
"size": [150, 50]
},
"behavior": {
"value": "{0.005 1/(Predators * Years)} * [Predators]",
"units": "1 / Years"
}
},
{
"type": "VARIABLE",
"name": "Predator Birth Rate",
"display": {
"coordinates": [300, 50],
"size": [120, 50]
},
"behavior": {
"value": "{0.0002 1 / (Prey * Years)} * [Prey]",
"units": "1 / Years"
}
},
{
"type": "VARIABLE",
"name": "Predator Death Rate",
"display": {
"coordinates": [637, 340],
"size": [120, 50]
},
"behavior": {
"value": 0.25,
"units": "1 / Years"
}
},
{
"type": "STOCK",
"name": "Predators",
"behavior": {
"initial_value": 20,
"non_negative": true,
"units": "Predators"
},
"display": {
"symbol": "🦊",
"interactive": true,
"interactive_min": 0,
"interactive_max": 100,
"coordinates": [506, 212],
"size": [100, 40]
}
},
{
"type": "FLOW",
"name": "Prey Births",
"from": null,
"to": "Prey",
"display": {
"from_coordinates": [185, 62]
},
"behavior": {
"value": "[Prey]*[Prey Birth Rate]",
"non_negative": true,
"units": "Prey / Years"
}
},
{
"type": "FLOW",
"name": "Prey Deaths",
"from": "Prey",
"to": null,
"display": {
"to_coordinates": [185, 422]
},
"behavior": {
"value": "[Prey]*[Prey Death Rate]",
"non_negative": true,
"units": "Prey / Years"
}
},
{
"type": "FLOW",
"name": "Predator Births",
"from": null,
"to": "Predators",
"display": {
"from_coordinates": [556, 60]
},
"behavior": {
"value": "[Predators]*[Predator Birth Rate]",
"non_negative": true,
"units": "Predators / Years"
}
},
{
"type": "FLOW",
"name": "Predator Deaths",
"from": "Predators",
"to": null,
"display": {
"to_coordinates": [556, 420]
},
"behavior": {
"value": "[Predator Death Rate]*[Predators]",
"non_negative": true,
"units": "Predators / Years"
}
},
{
"type": "LINK",
"from": "Prey Birth Rate",
"to": "Prey Births"
},
{
"type": "LINK",
"from": "Prey Death Rate",
"to": "Prey Deaths"
},
{
"type": "LINK",
"from": "Predators",
"to": "Prey Death Rate"
},
{
"type": "LINK",
"from": "Predator Death Rate",
"to": "Predator Deaths"
},
{
"type": "LINK",
"from": "Predator Birth Rate",
"to": "Predator Births"
},
{
"type": "LINK",
"from": "Prey",
"to": "Predator Birth Rate"
}
],
"visualizations": [
{
"type": "TIME_SERIES",
"name": "Prey and Predators",
"elements": [
"Prey",
"Predators"
]
},
{
"type": "TABLE",
"name": "Details Table",
"elements": [
"Predators", "Prey", "Predator Deaths", "Predator Births", "Prey Births", "Prey Deaths"
]
}
]
}{
"engine": "SIMULATION_PACKAGE",
"simulation": {
"algorithm": "RK1",
"time_start": 0,
"time_length": 20,
"time_step": 0.2,
"time_units": "WEEKS"
},
"elements": [
{
"type": "STOCK",
"name": "S",
"description": "Initial number of susceptible individuals.",
"behavior": {
"initial_value": 100
},
"display": {
"interactive": true,
"interactive_min": 0,
"interactive_max": 100,
"coordinates": [140, 60],
"size": [100, 40]
}
},
{
"type": "STOCK",
"name": "I",
"description": "Initial number of infected individuals.",
"behavior": {
"initial_value": 3
},
"display": {
"coordinates": [140, 190],
"size": [100, 40],
"interactive": true,
"interactive_min": 0,
"interactive_max": 100
}
},
{
"type": "STOCK",
"name": "R",
"description": "Initial number of recovered individuals.",
"behavior": {
"initial_value": 0
},
"display": {
"coordinates": [140, 310],
"size": [100, 40],
"interactive": true,
"interactive_min": 0,
"interactive_max": 100
}
},
{
"type": "VARIABLE",
"name": "γ",
"behavior": {
"value": 0.3
},
"display": {
"coordinates": [50, 185],
"size": [50, 50],
"interactive": true,
"interactive_min": 0,
"interactive_max": 1
}
},
{
"type": "VARIABLE",
"name": "β",
"behavior": {
"value": 0.01
},
"display": {
"coordinates": [50, 40],
"size": [50, 50],
"interactive": true,
"interactive_min": 0,
"interactive_max": 0.05
}
},
{
"type": "FLOW",
"name": "Infection",
"from": "S",
"to": "I",
"behavior": {
"value": "[β] * [S] * [I]"
}
},
{
"type": "FLOW",
"name": "Recovery",
"from": "I",
"to": "R",
"behavior": {
"value": "[γ] * [I]"
}
},
{
"type": "LINK",
"from": "β",
"to": "Infection"
},
{
"type": "LINK",
"from": "γ",
"to": "Recovery"
}
],
"visualizations": [
{
"type": "TIME_SERIES",
"name": "Disease Spread",
"elements": ["I", "R", "S"]
}
]
}{
"elements": [
{
"type": "VARIABLE",
"name": "Population",
"display": {
"coordinates": [140, 100],
"size": [120, 50]
}
},
{
"type": "VARIABLE",
"name": "Births",
"display": {
"coordinates": [90, 280],
"size": [120, 50]
}
},
{
"type": "VARIABLE",
"name": "Carrying Capacity",
"display": {
"coordinates": [480, 60],
"size": [130, 50]
}
},
{
"type": "VARIABLE",
"name": "Deaths",
"display": {
"coordinates": [430, 180],
"size": [120, 50]
}
},
{
"type": "LINK",
"from": "Population",
"to": "Deaths",
"behavior": {
"polarity": "POSITIVE"
}
},
{
"type": "LINK",
"from": "Population",
"to": "Births",
"behavior": {
"polarity": "POSITIVE"
}
},
{
"type": "VARIABLE",
"name": "Net Growth",
"display": {
"coordinates": [320, 320],
"size": [120, 50]
}
},
{
"type": "LINK",
"from": "Deaths",
"to": "Net Growth",
"behavior": {
"polarity": "NEGATIVE"
}
},
{
"type": "LINK",
"from": "Carrying Capacity",
"to": "Deaths",
"behavior": {
"polarity": "NEGATIVE"
}
},
{
"type": "LINK",
"from": "Births",
"to": "Net Growth",
"behavior": {
"polarity": "POSITIVE"
}
},
{
"type": "LINK",
"from": "Net Growth",
"to": "Population",
"behavior": {
"polarity": "POSITIVE"
}
}
]
}{
"engine": "SIMULATION_PACKAGE",
"name": "Bathtub",
"simulation": {
"algorithm": "RK1",
"time_start": 0,
"time_length": 20,
"time_step": 1,
"time_units": "MINUTES"
},
"elements": [
{
"type": "STATE",
"name": "Is Filling",
"display": {
"coordinates": [630, 110],
"size": [100, 40]
},
"behavior": {
"initial_value": true
}
},
{
"type": "STATE",
"name": "Is Bathing",
"display": {
"coordinates": [630, 230],
"size": [100, 40]
},
"behavior": {
"initial_value": false
}
},
{
"type": "STATE",
"name": "Is Draining",
"display": {
"coordinates": [630, 350],
"size": [100, 40]
},
"behavior": {
"initial_value": false
}
},
{
"type": "TRANSITION",
"name": "Done Filling",
"from": "Is Filling",
"to": "Is Bathing",
"behavior": {
"trigger": "TIMEOUT",
"value": 5
}
},
{
"type": "TRANSITION",
"name": "Bath Over",
"from": "Is Bathing",
"to": "Is Draining",
"behavior": {
"trigger": "TIMEOUT",
"value": 5
}
},
{
"type": "STOCK",
"name": "Bathtub",
"description": "The bathtub starts empty.",
"display": {
"coordinates": [390, 220],
"size": [100, 40]
},
"behavior": {
"initial_value": 0,
"non_negative": true,
"units": "Liters"
}
},
{
"type": "FLOW",
"name": "Filling",
"description": "Water fills at a rate of 10 liters per minute.",
"from": null,
"to": "Bathtub",
"display": {
"from_coordinates": [440, 90]
},
"behavior": {
"value": "if [Is Filling] then\n 10\nend if",
"non_negative": true,
"units": "Liters/Minute"
}
},
{
"type": "FLOW",
"name": "Draining",
"description": "Water drains at a rate of 20% per minute.",
"from": "Bathtub",
"to": null,
"display": {
"to_coordinates": [440, 400]
},
"behavior": {
"value": "if [Is Draining] then\n {0.2 1/Minute} * [Bathtub]\nend if",
"non_negative": true,
"units": "Liters/Minute"
}
},
{
"type": "LINK",
"from": "Is Filling",
"to": "Filling"
},
{
"type": "LINK",
"from": "Is Draining",
"to": "Draining"
}
],
"visualizations": [
{
"type": "TIME_SERIES",
"name": "Bathtub Volume",
"elements": [
"Bathtub"
]
},
{
"type": "TIME_SERIES",
"name": "States",
"elements": [
"Is Filling",
"Is Bathing",
"Is Draining"
]
}
]
}