Modelling System Dynamics

Last updated on 26th March 2024

This is an experimental feature!

Experimental features are early versions for users to test before final release. We work hard to ensure that every available Simudyne SDK feature is thoroughly tested, but these experimental features may have minor bugs we still need to work on.

If you have any comments or find any bugs, please share with support@simudyne.com.

This will provide an overview of the components and features available using System Dynamics (SD) modelling in the Simudyne SDK.

In order to use System Dynamics Modelling please add simudyne-core-abm_2.11 to your pom.xml dependencies

Defining a Model

This is simply making sure your main model class extends from SystemDynamicsModel

Creating a Model (java)
import simudyne.core.sdm.SystemDynamicsModel; 

public class MyModel extends SystemDynamicsModel {}

Model Settings

While you're able to use existing annotations for defining model settings, System Dynamics has a defined model settings annotation in SystemDynamicsSettings.

At the moment this is used primarily to define the 'resolution' of the model with the default set at 1.

Setting a Resolution of 4 (java)
import simudyne.core.sdm.SystemDynamicsModel; 
import simudyne.core.sdm.annotations.SystemDynamicsSettings;

@SystemDynamicsSettings(resolution = 4)
public class MyModel extends SystemDynamicsModel {}

Stock

When annotating a double value as a Stock (it's generally good practice to use doubles in SD modelling especially at higher resolutions) while you can set a name parameter this is not currently used by the console.

You can, however, set a boolean called nonNegative (default = false) to true. This will alter the behaviour of your stock & flow model. In short, when the values of a stock are to be manipulated by a flow this puts in a hard block such that a flow cannot take from this stock, and that it will not go below 0.

Non-Negative Processing

If you have two values taking from a stock set to Non-Negative there is the potential that changes to your model will alter which flow 'gets there first' if there are no dependency conflicts. NonNegative is meant to be a 'shortcut' simplification to accurately model representations of real world stocks.
Creating Stocks (java)
@Stock public double StockA = 0;

@Stock(nonNegative = true)
public double StockB = 500;

Flow

A flow must be a function that returns a double. In order to properly build the dependencies such that loops and order of calculations are represented, you'll need to specify the arguments in the annotation.

This will look at the current values of the provided arguments and pass them into your function - calculating them first if that's not been done so this time step.

As mentioned in the previous page, not all flows need both a to and from stock so you'll only need to provide them as necessary.

Creating Flows (java)
  @Flow(to = "StockA")
  public double Flow1() {
    return 200.0;
  }

  @Flow(from = "StockA")
  public double Flow2() {
    return 100.0;
  }

  @Flow(
    from = "StockA",
    to = "StockB",
    args = {"StockA", "Flow1", "Flow2"}
  )
  public double Flow3(double sa, double f1, double f2) {
    return sa + f1 - f2
  }

Intermediate

An intermediate is a special kind of variable that you can provide arguments to in order to resolve said intermediate as a function. This works similar to flows, and in the context of System Dynamics modelling is used in determining the topological ordering of calculations.

You can specify a fixed variable as an intermediate if desired as well.

Creating Intermediates (java)
@Intermediate public double Inter1 = 0.5;

@Intermediate(args = {"StockA", "StockB"})
public double Inter2(double valA, double valB) {
  return valA - valB
}

Delay

A delay is a special function made available in SystemDynamicsModel. There are two steps to using it, defining the delay and annotating the target.

Creating a DelayTarget and Calling from a Flow (java)
  @DelayTarget
  @Intermediate public double flowRate = 20;

  @Flow(
    from = "sourceStock",
    to = "destinationStock",
    args = {"flowRate", "delayTime"}
  )
  public double delayedFlow(double fr, double dt) {
	if (getTime() > 10) {
	  return delay("flowRate", dt);
	} else {
	  return fr;
	}
  }

As you'll notice above we have a condition to check the time of the model. Because of the way the delay function works (looking for the value at t-n), it's often likely if you've not set up your checks correctly that you would get a DelayException for being out of bounds.

You can mitigate this by setting a defaultValue. This way if the time your delay is looking for is before the start of the model, or out of bounds you'll return the default value.

Setting a Default Value for a Delay Target (java)
  @DelayTarget(defaultValue = 0)
  @Intermediate public double flowRate = 20;

  @Flow(
    from = "sourceStock",
    to = "destinationStock",
    args = {"delayTime"}
  )
  public double delayedFlow(double dt) {
	return delay("flowRate", dt);
  }

Step/Setup Functions

SystemDynamicsModel has two major functions like most models - setup and step. You optionally can override these functions.

(java)
  @Override
  public void setup() {
	super.setup();
	// Some other actions
  }

  @Override
  public void step() {
    super.step();
	System.out.println("The time is: " + getTime());
	// Some other actions
  }

TimeSteps

A SystemDynamicsModel effectively has a Major and Minor time step. The major time step is the main calculation, what happens when you advance via the console.

However, because you can set a differing resolution there is a minor step as well that runs within the span of a single major step. To accurately understand the timing of this there are two main functions to use.

Explaining timesteps at higher resolutions (java)
@SystemDynamicsSettings(resolution = 4)
public class MyModel extends SystemDynamicsModel {

  @Override
  public void step() {
	super.step();
	System.out.println("The time is: " + getTime()); // Will always return a whole number of the major step
  }
  
  @Flow(
	from = "StockA",
	to = "StockB",
	args = {"StockA", "Flow1", "Flow2"}
  )
  public double Flow3(double sa, double f1, double f2) {
	System.out.println("The time is: " + getTime()); // Because the resolution is 4. We'll get 0.0, 0.1, 0.2, 0.3, and 1.0 to denote Major.Minor steps.
	return sa + f1 - f2
  } 
}

As noted in the explanation of System Dynamics modelling. You can define a resolution of a very high number. Often, however, this has the decreased benefit of being higher fidelity, but costing performance as you basically run the calculations multiple times for a single tick.

Often a resolution of 4 or 8 is sufficient, but you should test and benchmark this for your model to determine the right breakpoint.

Interaction with Console as of 2.1

As of 2.1, there is no direct System Dynamics support within the console. However, because SystemDynamicsModel is based on a Model we're able to use it within the console as-is by specifying additional annotations to Stock/Flow/Intermediates by usage of the Input & Variable annotations.

Based on feedback from modellers using SD in 2.1 we'll begin to develop what features they'll want from integrating SD in the console. This could be as simple as showing annotated stocks/flows as charts, a panel allowing you to directly view the calculation dependencies of your model or even a CLD style diagram.