CSDMS Model Interface Standards
For you model application to become a component that can be linked in meaningful ways to other model components, it must have a application programming interface (API). CSDMS asks model contributors to provide a minimal set of functions that allow a calling program to control the component's execution. The minimal set of interface functions would be those that initialize, run, and finalize a component model - the IRF interface. For more complete linkages, your API should also contain value accessors and mutators so that an application can query and change state variables of your model.
The IRF Interface
Numerical models can generally be subdivided into three phases: set up, execution, and teardown. The set up phase occurs before time stepping begins and initializes the model. The execution phase is the guts of your model and will be most everything within the main time loop of the model. The teardown phase occurs after time stepping and acts to clean up the model simulation.
There are many models that are time-independent and so do not have a time loop but that is not to say that they don't have an execution step. In this case, the model calculations can be thought of as a time-stepping model with just one time step. I hope that the following sections will have some of this more clear.
Initialize (Model Set Up)
Before a model enters into its time-stepping loop, it will usually execute a set of commands necessary to set up the subsequent model simulation. This can be thought of as the initialization step - the lines of code before the time loop. The initialize step will put your model into a valid state that is ready to be executed. Mostly this will be initializing variables or grids that will subsequently be used within the execution step. Temporary files that the execution step will read from or write to should also be opened here. Note however that any files that are intended as an interface to a user, should not be used here. User interfaces belong outside of the IRF modeling interface.
Things to include in your model's initialization function:
- Initialize variables.
- Allocate memory.
- Open temporary files
- Stuff that is done once, and is before the time loop.
Things not to include in your model's initialization function:
- User interfaces. Your initialization function should not include a user interface (graphical user interface, or a command line interface, for example). This should be taken care of by the calling application.
A typical interface for model initialization:
int init_my_model ( char **strs );
In this case the initialization function takes an array of strings as input. The strings might be names of input files to read from, or could be key/value pairs that set particular model variables. These are just suggestions. It may make sense for you model to initialize itself in a somewhat different way. The main point is that you provide an entry point to your initialization step and document your particular interface.
Run (Model Execution)
Your model's execute step should run your model for a particular amount of time (simulation time, that is). Generally speaking, this will be the lines of code that are within you time loop. If your model is time-independent, it should run just once though. Ideally, it should save this state so that if it were to be called again it would not have to run through its calculations again; rather it would just maintain its current state, saving computation time.
Things to include:
- Code that updates the state of you model.
Things not to include:
- User interfaces. This could include, for instance, plotting of the model's state as it's running. These things should be taken care of by the application that runs your component.
- Writing data to output files. This is just another interface and the calling application should do this.
int run_my_model (double time);
Finalize (Model Termination)
The finalize step cleans up after your model. The main purpose of this step to make sure that all resources that your model acquired through its life have been freed. Most often this will be freeing allocated memory, but could also be freeing file or network handles. As we have said before, user interfaces should be left out of this step. Following this step, the model should be left in an invalid state such that its run step can no longer be called.
Things to include:
- Freeing memory.
- Closing files.
Things not to include:
- User interfaces such as writing to an output file intended to be read by a user.
int finalize_my_model (void);
Your Model Application
Notice that the above interfaces have excluded any user interfaces. As we've stated above, the application should take care of user interfaces. With the above interfaces, your model application will consist of a call to your initialize function, followed by your run function (possibly wrapped in a for loop, say), followed by a call to your finalize function. Between these calls, will be your user interface. The particular user interface that you decide to use is up to you.
Without any user interface, your application might look something like the following pseudocode,
CALL model init function REPEAT CALL model run function for some duration INCREMENT time UNTIL time >= end time CALL model finalize function
Since an application is not of much use if it doesn't interface in some way with a user, you need to add a user interface. The pseudocode for this might look like,
DISPLAY GUI to collect initialization data CALL model init function REPEAT CALL model run function for some duration GET model data PRINT data to an output file DISPLAY data to screen UNTIL time >= end time PRINT final data to file CALL model finalize function
Notice now that your application requires your model to provide an interface that exchanges data between it and the application. From this example (highlighted), the application assumes that your model provides and interface to get data from it. Such methods are sometimes referred to as getters or accessors. Likewise, if you want an application to be able to change data within your model, you must provided an implementation for a setter (or mutator) interface.
Model Accessors and Mutators
The IRF interface described above allows a calling application to control the execution of your model. The application may be the one that you have already written. In which case, the new interface has really just acted to clean up you code and hopefully made it easier to maintain. However, the new interface has also made it easier for someone else to take you model and wrap it inside another application. Because the IRF interface provides a way to control the model's execution, ultimately the new application will be similar to yours. What could do though is put a completely new user interface on your model. For instance, someone could wrap your model in an application that reads input from a new file format, or maybe from a graphical user interface.
You may have noticed though that there are still a couple of things missing from this interface that would be useful. For your model to be able to meaningfully communicate with other models, you should be able to exchange values that your model calculates. Furthermore, your model should provide a means for another model to set particular values of itself. One way of doing this is through a getter and setter interface. As we have seen, a barebones IRF interface is certainly valuable but the more getter and setter methods your model has, the more useful it will be in linking with other models.
If an application wishes to access values that your model calculates, your model should implement some kind of getter method (or methods). This allows you to control what data your model shares with other programs and how it shares that data. There are two ways to transfer the data. The first is to give the calling program a copy of the data. The second is to give the actual data that is being used by the model (in c, this would mean passing a pointer to a value). The first is preferable as it hides the implementation details of your model from the calling program and also limits what the calling program can do to your model. However, the downside is that the first method will be slower (and could be significantly slower depending on the size of the data being transferred).
Things to include in a getter:
- Sanity check
- Copy data
int my_model_get_value (char* value_str, double** value_grid);
If you will want another program to be able to set data within your model, you should provide a setter interface for those values. Variables within your model should only be accessed and changed through methods that you define. This ensures that programmers are not able to change values that they shouldn't be able to, and so help to eliminate bugs. This also detaches the programmer using your interface from your model implementation, thus freeing you to change details of your model without an application programmer having to make any changes.
You can also have your setting perform task other than just setting data. It could be useful, for instance, if the setter checked to make sure that the new data is valid. After the mutator method sets the data it should ensure that the model is still in a valid state. If not, the method should indicate that an error was encountered (possible through a return value).
Things to include in a setter:
- Sanity check
- Copy data
int my_model_set_value (char* value_str, double** new_value);