Working with equations : User-supplied functions

User-defined functions

There are three different mechanisms for users to supply functions for use in expressions:

  1. user-defined macros, which provide a short-hand for long or complex expressions that would otherwise have to be used repeatedly in expressions.
  2. user-defined external procedures, written in a programming language such as Tcl or C++.
  3. model-fragment function definitions (introduced in Simile v6), in which the behaviour of a function is implemented by a separate Simile model.

User-supplied function declarations and definitions should be put in files in the Functions directory in Simile's local data tree. This tree is created automatically when you first run Simile. Its location is:

  • On Windows: "My Simile Files", under "Documents" (Windows 7 or Vista) or "My Documents" (earlier Windows versions)
  • On Linux: .simile, in the user's home directory
  • On MacOS: Simile, in the user's home directory

Declarations and definitions in the Functions directory in Simile's installation tree (e.g., "Program files/Simile54") are for functions treated as "Built-In", i.e., expected to be available in any Simile system. It is worth looking at these for guidance in writing your own functions, as the formats are the same.

In: Contents >> Working with equations

User-defined functions : Macro definitions

Macro definitions

Macro definitions provide a shorthand for long, complex expressions that would otherwise have to be used in equations, possibly in several different elements. The definitions are stored in one or more external files, which are read each time Simile starts. Users may edit the files to include user-defined macros.

Each new macro is defined in a new line in any file with the extension .pl in the Functions directory. There are two places where this directory can be; one is within the Simile program files tree (for built-in functions), and the other is under Simile's local directory (for functions to be treated as user-defined). Any functions added by modellers should be placed in the latter location;  this has the effect that when a model requiring the functions is saved, they are marked as user-defined, and if the model is subsequently opened on a system where the function definitions are not present, a warning is displayed saying which user-defined functions are missing.

The format of the macro definition line is:

f(X1, X2 ... Xn) --> F(X1, X2 ... Xn).

where:

  • f is the name of the user-defined function. This has the format of a Prolog atom or variable, so it must start with a letter, and unlike the built-in function names it is case-sensitive.
  • X1, X2 … Xn are a series of one or more variable identifiers. These also have the format of Prolog atoms or variables, so they must start with letters and are case sensitive. These formal arguments to the function are replaced when the function is used, by the actual argument values. If the function requires no arguments, you must place a pair of single quotes between the empty parentheses.
  • F(X1, X2 … Xn) represents any expression that could be used in an element's equation. The variable identifiers can be used as quantities anywhere in this expression. This is the macro itself. As with any expression in the equation language, it may extend over more than one line.
  • The symbol '-->' means 'maps onto'. It, and the final period, are part of Prolog syntax.
  • Next line shows how to write 0-ary (no arguments) macro definitions (leave parentheses out) and macro definitions calling 0-ary functions or macros (put empty atom in parentheses).
    init_time --> at_init(time('')).
    Note the argument in the call is two single quotes, not a double quote.
     

The function, as it appears on the left side of the arrow, can then be used in any Simile equation, with any sub-expression taking the place of each of the variable identifiers. The result returned by the function will be the same as that which would have been returned by the expression on the right hand side, if the same sub-expressions had been substituted for the variable identifiers.

The .pl files distributed with Simile (and installed in the Functions directory in Simile's program files) contain a number of examples of function definitions. These include the following:

  • subtotals(Arr): Takes an array and returns another array of the same size containing the totals of all the values up to that point in the original array. e.g., subtotals([1,2,4,3]) = [1,3,7,10].
  • rankings(Arr) Takes an array and returns an array of integers of the same size each representing the position in the sequence of largest to smallest (largest = 1) of the corresponding value in the original array. e.g., rankings([8.2, -5.1, 2.5]) = [1,3,2].
  • with_greatest(Arr1, Arr2) Takes two arrays and returns the element of the second in the position corresponding to the largest-valued element of the first
  • colin(Arr) Takes an array and on each time step returns an integer, with the probability of each value being proportional to the value at that position in the original array.

Comment lines, starting with a %, can be included in this file, and standard multi-line comments bounded by /*...*/ can be used.

If there is a syntax error in a user function definition, this will cause a warning to be produced when Simile is started. The other definitions will still be usable.

In: Contents >> Working with equations >> User-defined functions

User-defined functions : External procedures

External procedural functions

To include user-defined external procedures, there are three files:

  1. A file under the Functions directory with the .pl extension. This file can also contain macro definitions. Procedurally defined functions that are treated as being built-in to Simile go in Simile's installation tree, while those that are treated as user-defined go in the local information tree. The declarations take one of two possible forms:

    function(Name, ResultType, ArgTypes).
    This form is used for ordinary, deterministic functions.
    sample(Name, ResultType, ArgTypes).
    This form is used for functions which give a new value each time they are called, even if the arguments do not change. This is used for stochastic functions such as gaussian_var, and is required because Simile otherwise tries to be lazy -- it only re-evaluates a function if the arguments change.

    In either form, "Name" is the name of the function, which is used in Simile's equation language. "ResultType" the type of the result - one of int, real, boolean or any - and "ArgTypes" a list of argument types in the same form. These allow the Prolog equation parser to accept this function in equations, and to put it into the target language program as a procedure call wherever it is used.

  1. procs.cpp -- This is in the same Functions directory as the function declaration. It contains the C++ implementation of the function. When the model is built in C++, during compilation of the generated code, this file is included (via support1.cpp) and linked into the resulting model library.
  1. procs.tcl -- contains the Tcl implementation of the function. This is sourced just prior to the model program, but not in global scope so any global variables used by the procedure need to be declared both inside it and out.

These files are stored in the Functions directory of the Simile program files tree. If you wish to build models in both C++ and Tcl, using the same function name, you must include the function definition is both procs.cpp and procs.tcl.

It is generally not a good idea to use global variables in the function definitions, because if the functions are used in more than one place in the model, a value set when running one instance of it may be used when running another instance.

In: Contents >> Working with equations >> User-defined functions

User-defined functions : Model Fragments

Model Fragments as Function Definitions

This feature, introduced in Simile v6, is really, really easy to use. Suppose you need to have a particular function available in your model, and you have no idea how to program it in c++ or write a macro expression for it, but you can easily make a simple Simile model that implements the function. One option would be to build such a model, and copy it to every part of the model you are working on where you need the function it provides. The trouble is, this would make the diagram confusing and the model would be harder to maintain, since if you changed the implementation of the function you would have to change all the occurrences of the model fragment that carries it out.

Now you can keep the model fragment somewhere else, and refer to it wherever you like by means of an ordinary function. The fragment goes in a directory called Fragments which is a subdirectory of either of the Functions directories for user-defined functions in general. The name of the function it implements, and the components of the fragment which correspond to the result and arguments, are identified by the name of the fragment file, which has the format 

functor,result,arg,... .sml

For instance, suppose you want a function that returns the first prime number greater than its argument. You could make a model fragment that looks like this:

Model fragment implementing 'next prime number' function

This model takes a number in the 'start' variable, and generates the next prime number up from it in the 'next' variable. It uses two nested iterative submodels. The outer one tries each odd number, starting from 'start', until one is found to be prime. The inner one applies the test for primeness, dividing the candidate by each odd number starting from 3 until either an exact factor is found, or the next divisor is greater than the square root of the candidate. If a factor is found, the outer submodel moves on to the next candidate and the inner one starts again.

So, we have our model, how do we make the function? By saving it in the Fragments directory with a name made of the following parts:

  • The name of the function, in this case 'next_prime'
  • The caption of the model component that has the result, in this case 'next'
  • the captions of all the arguments in the order in which they will appear in the function call, in this case just the one: 'start'

These are all separated by commas and the name finishes with the .sml extension as for any Simile model, so the full name is

next_prime,next,start.sml

Now you can use the function in any equation, e.g. "next_prime(z)>50"

Points to note:

  • The argument components must be variable parameters as shown above.
  • The result and argument components must be in the top level of the model fragment, i.e., not in a submodel
  • ​The fragment can include compartments, in which case the function will have a state and its output will depend on its inputs at earlier times
  • If you use an array as an argument to a function where it is a single value in the definition, you will get a separate instance of the fragment for each element of the array
  • The internal components of the fragments will appear in the model explorer during execution and their values can be inspected
  • Fragments can include any Simile feature, including functions defined by other fragments (but avoid circular definitions!)

In: Contents >> Working with equations >> User-defined functions