Working with submodels : Iterative submodels

Iterative submodels

Motivation

System-dynamics models are usually a very naturalistic way of describing a process that takes place. The actual way in which one quantity influences another in the problem domain is mirrored by one variable influencing another in the model. If a calculation must be repeated a certain number of times with each value depending on the previous one, it can be done in a multi-instance submodel using the in_preceding() function to allow each instance to refer to values from the previous one.

However, we may wish to create a model of a system in which our best understanding of how a certain quantity is evaluated involves a procedural computation in which an instruction loop is repeatedly executed until some exit condition is met. Using a multi-instance submodel is wasteful because the number of iterations needed may vary widely, and we are not interested in values from iterations before the last one. An example is the Ball-Berry model of stomatal conductance. Although in nature stomatal conductance is the result of a purely incremental process, the easiest way to evaluate it in a model of plant physiology is to include an iterative calculation with a finishing condition. If performing a calculation like this in a procedural programming language such as c++, you would use a 'while' loop. Such a calculation could be included using Simile's ability to implement a submodel's behaviour using a piece of code in c++, but this is not ideal, since:

  • One shouldn't need to learn a programming language to use Simile
  • It defeats the point of the model's behaviour being visually explicit

Simile therefore allows iterative processes to be represented graphically. This involves a mechanism to specify that the components inside a submodel are evaluated repeatedly until a specified end condition is met. With this system, a modeller can not only implement iterative approximations to natural processes such as the Ball-Berry model -- they can also create graphically explicit procedures for solving abstract problems, such as finding prime numbers or efficiently sorting an array.

Implementation

An iteration symbol and a property for influence arrows are included, to allow a loop to be executed multiple times within one time step. Such looping may be required, for example, when the value of a variable cannot be calculated directly from others, but is found by trying many successive values to minimise some error function.

The basic elements of an iterative loop are as follows.

The iteration symbol contains the condition that marks the successful convergence of the iteration. An influence arrow coming from the alarm symbol can be used as an argument to the function iterations( ). This function returns the number of iterations made so far. This function can be used to set the initial value (also called the guess) for the loop, i.e. when the number of iterations so far is equal to zero. If the number of iterations so far is one or more, then the result of the last calculation should be used. Since the last calculation depends on the result calculated from the guess, a circular loop of influences is present. Normally, Simile would reject this loop at build time, but the definition of the iterations() function makes it explicit that the influence from the alarm symbol refers to its value on the previous iteration, and therefore cannot be part of a circularity.

You do not have to use the iterations( ) function; if the results of previous iterations are irrelevant, as for instance in a generate-and-test algorithm, you may not need to have any influences from the alarm symbol at all. Alternatively, if each result does depend on previous ones but the actual number of iterations is irrelevant, you can use the boolean value of the alarm symbol directly to choose between starting a new iterative approximation (true) and refining the result of the previous iteration (false).  In this case you would get a circularity, but setting a property of the influence arrow: "Use values made in same time step" to true, allows the loop to be processed. You also need to set this property on influences that convey the result of the last iteration step for use at the start of the following step. Influence arrows with this property set are drawn with a dashed line, and the equation that uses their value gets the one from the previous iteration rather than the current one. To set this property for an influence arrow, double-click on it to invoke the property dialogue box.

It is unlikely that any of the above description is comprehensible without an example. The included example model for Ball-Berry stomatal conductance poses a problem for conventional System Dynamics modelling tools because it depends upon the solution of the following pair of simultaneous equations.

Gs = g0 + g1 * A * H / Ca

A = Gs * AQ

Apart from the external parameters, g0, g1, H, Ca and AQ, the equation for Gs depends only on A, and the equation for A depends only on Gs. In this implementation, the loop is opened, by introducing a place-holder for Gs, called Gs_0. This variable is calculated from A. A is calculated using Gs. A guess is made for the initial value of Gs. Subsequently, Gs is set to the last calculated value of Gs_0. The influence arrow from Gs_0 to Gs is dashed, indicating that the value from the previous iteration is to be used. The alarm symbol terminates the iterative loop when Gs and Gs_0 differ by less than one part in one thousand.

Caution

Alarm submodels should always behave as if everything in the submodel is executed repeatedly until the finishing condition is reached. However, Simile always tries to do as little work as possible, so this may not actually happen. For instance if the submodel contains constants, these are not set in each loop but set just once before the loop starts.

Also, if the exit condition in the iteration symbol does not depend directly or indirectly on a certain value in the submodel, then Simile will assume that that value is not relevant to the loop calculation and it will instead be calculated only once after the loop has finished. For instance, we made a test model that carried out a merge sort by iterating until the required number of merges were done. Since we know that for an array of n elements, the number of merges needed is log2(n), we just had a condition that counted this number of iterations. However, this resulted in only one actual merge being done rather tha a series to build sorted sublists of increasing lengths. The fix was to make the exit condition depend on the output array, to force it to be merged every cycle. This could have been done by actually checking that each value was larger than the preceding one, but it is simpler just to check for some dummy never-true condition e.g. sum([out])!=sum([out]) as well as the right number of iterations having been done.

Another problem is that if you make a mistake when building an iterative submodel, you may end up with the exit condition never being met. The model will then go into an endless loop. To escape the loop, hit 'pause' on the run control, and wait for the 'model seems to have got stuck' message -- you can then exit the model and try to fix the problem.

In: Contents >> Working with submodels