Writing code for APSIM

The .NET languages are used to write models for APSIM. Either C#.NET or VB.NET can be used. What follows is a guide that shows the various language features that can be used to build an APSIM Model or script.

Static example

Consider the following code example that calculates a gross margin:

VB.NET

Imports System
Imports ModelFramework
Public Class GrossMargin
   Private ResetBank As Boolean
   <Input()> Private Fertiliser As Double = 0
   <Input()> Private irrig_tot As Double = 0
   <Input()> Private Yield As Double = 0
   <Input()> Private grain_protein As Double = 0
   <Input()> Private StageName As String = ""
   <Input()> Private plant_status As String = ""
   <Input()> Private maxt As Double
   <Param()> Private NCost As Double = 0
   <Param()> Private NApplicationCost As Double = 0
   <Param()> Private WaterCost As Double = 0
   <Param()> Private Price As Double = 0
   <Param()> Private MinimumProtein As Double = 0
   <Param()> Private ProteinIncrement As Double = 0
   <Output()> <Units("$")> Public Bank As Double
   <[Event]()> Public Event NegativeBank()
   <EventHandler()> Public Sub OnPost()
      ' Reset the bank to zero the day after harvest.
      If (ResetBank) Then
         ResetBank = False
         Bank = 0
      End If
      ' Subtract fertiliser cost from the bank
      If (Fertiliser > 0) Then
         Bank = Bank - (NCost * Fertiliser + NApplicationCost)
      End If
      ' Subtract irrigation costs.
      If (irrig_tot > 0) Then
         Bank = Bank - WaterCost * (irrig_tot / 100)
      End If
      ' Add harvest return to the bank
      If (StageName = "harvest_ripe" Or plant_status = "dead") Then
         Dim FullPrice As Double = Price + (grain_protein - MinimumProtein) _
                                    * 2 * ProteinIncrement
         Bank = Bank + FullPrice * (Yield / 1000)
         ResetBank = True
      End If
      ' Publish event if bank falls below zero.
      If (Bank < 0) Then
         RaiseEvent NegativeBank()
      End If
   End Sub
End Class

 

C#.NET

using System;
using ModelFramework;
class GrossMargin
    {
    private bool ResetBank;
    [Input]              private double Fertiliser = 0;
    [Input]              private double irrig_tot = 0;
    [Input]              private double Yield = 0;
    [Input]              private double grain_protein = 0;
    [Input]              private string StageName = "";
    [Input]              private string plant_status = "";
    [Param]              private double NCost = 0;
    [Param]              private double NApplicationCost = 0;
    [Param]              private double WaterCost = 0;
    [Param]              private double Price = 0;
    [Param]              private double MinimumProtein = 0;
    [Param]              private double ProteinIncrement = 0;
    [Output][Units("$")] public double Bank;
    [Event]              public event NullTypeDelegate NegativeBank;
    [EventHandler]       public void OnProcess()
        {
        // reset the bank to zero the day after harvest.
        if (ResetBank)
            {
            ResetBank = false;
            Bank = 0;
            }
        // Subtract fertiliser cost from the bank
        if (Fertiliser > 0)
            Bank = Bank - (NCost * Fertiliser + NApplicationCost);
        // subtract irrigation cost.
        if (irrig_tot > 0)
            Bank = Bank - WaterCost * (irrig_tot / 100);
        // Add harvest returnto the bank
        if (StageName == "harvest_ripe" || plant_status == "dead")
            {
            double FullPrice = Price + (grain_protein - MinimumProtein)
                                        * 2 * ProteinIncrement;
            Bank = Bank + FullPrice * (Yield / 1000);
            ResetBank = true;
            }
        // Publish event if bank falls bellow zero.
        if (Bank < 0)
            NegativeBank.Invoke();
        }
    }

The above example calculates a simple gross margin based on a bank balance. It subtracts the cost of fertiliser and irrigation water from this bank balance and adds in income based on simulated yield and grain protein.

Two things to note when creating your models. Firstly, make sure your class is declared as public. Secondly, ensure that your class is not inside a namespace. This will help ensure that APSIM is able to locate your class.

APSIM relies heavily on reflection tags to analyse the model source code to locate variables and methods.

    • The [Input] tag denotes that a value for this variable needs to be supplied by APSIM. In the example, APSIM will locate a variable called Fertiliser in another model and retrieve its value. APSIM will do this every day. If APSIM cannot find the specified variable, then it will throw a fatal error. This error can be prevented by specifying the Input as optional with a true parameter e.g. Input(true).

 

    • The [Param] tag denotes that the variable is a parameter. Like the Input tag, APSIM will supply a value but only at the beginning of the simulation. APSIM looks for parameters in the XML configuration for this model. When the code is part of a script component, the parameter values will be on the “Properties” tab. If APSIM cannot find the specified parameter, then it will throw a fatal error. This error can be prevented by specifying the Param as optional with a true parameter e.g. Param(true). An alias can also be specified for the parameter. For example, Param(“NitrogenCost”) double NCost tells APSIM to look for a parameter called “NitrogenCost” rather than “NCost”. This is sometimes useful.

 

    • The [Output] tag denotes a variable that APSIM can supply to other APSIM models when requested. Like parameters, an alias can be specified for the output e.g. Output(“GrossMargin”) public double Bank

 

    • The [Writable] tag identifies a writable property that can be set from another module. This tag can be used in conjunction with the Output tag.  Use the .Set() function for built in types or .SetObject() for ApsimType types.

 

    • The [Units(“$”)] tag supplies metadata to APSIM. The REPORT module writes the units to the output file.

 

    • The [EventHandler] tag signals to APSIM that the following method is an event handler that needs to be called whenever the APSIM event is published. The convention is that all handlers will have an On prefix. The name of the method (minus the On prefix) denotes the APSIM event name that will be trapped.

 

  • The [Event] tag signals that the following .NET event should be published to the whole APSIM simulation. Other components in APSIM will be able to subscribe to this event. The name of the event (NegativeBank in this example) will be the name of the event that APSIM sees. Events must be declared using one of the APSIM types. See the description of APSIM types below.

To retrieve a list of APSIM variables and events, use the “Variables” and “Events” components under the “Output file” in the simulation tree in the APSIM User Interface.

Dynamic Example

In a lot of cases, the static approach above provides sufficient functionality. In some cases though it is necessary to discover elements of a simulation as it is running and to then communicate directly with specific components. This section shows how to dynamically communicate with an APSIM simulation.

Links

Links are the starting point for access to all dynamic simulation functionality. To discover what other models are running in the same paddock as the model you’re writing then insert the following line into the class variables section:

VB.NET

<Link()> Private MyPaddock As Paddock

C#.NET

[Link] private Paddock MyPaddock;

This will then give you a “variable” that represents the paddock that your component is running in. You can then call methods on the variable to determine the paddock name, loop through all sub (child) paddocks etc.

Some examples (all in VB.NET):

To loop through all sub paddocks:

For Each SubPaddock As Paddock In MyPaddock.SubPaddocks
   ' Do something with the SubPaddock variable.
Next

To loop through all crop models in the paddock:

 

For Each Crop As Component In MyPaddock.Crops
   ' Do something with the Crop variable.
Next

Once you have a paddock variable you can then get a variable that represents another model in the simulation:

To iterate through a list of child models

The following code snippet (C#) shows a model iterating through all child models, casting each child to a FeedItem model and then calling the getAmount method on each.

 

[Link]
private Component My;
FeedItem feedItem
double amount = 0;
for (int i = 0; I < My.ChildrenAsObjects.Count; i++)
{
    feedItem = (FeedItem) My.ChildrenAsObjects[i];
    amount = feedItem.getAmount();
}


To tell the fertiliser model to apply some fertiliser:

Dim Fertiliser As Fertiliser = MyPaddock.ComponentByType("Fertiliser")
Fertiliser.Apply(50, 10, "NO3_N")

To tell the wheat model to sow:

Dim Wheat As Wheat = MyPaddock.ComponentByType("Wheat")
Wheat.Sow("Hartog", 100, 30, 250, 0, 0, "", 0, 0, 0, 0, "", 0, 0, 0, 0, 0)

Links can also be used to retrieve links to other components. So rather than using the ComponentByType method in the above examples, a link can be set up to point to a specific component:

<Link()> Dim WheatModel As Wheat
<Link()> Dim SoilWaterModel As SoilWat

APSIM will look for these models within the current ‘scope’ of the component that is running. Currently, this will be in the current paddock or the parent paddock.

nb. In your Solution you need to add a reference to DotNetProxies.dll (under your Model directory). This will soon change to use CSDotNetProxies.dll.

Variables
Once you have a link or variable that represents another model in the simulation you can then query that model for the values of its variables.

<Link()> Dim SoilWaterModel As SoilWat
...
Dim sw As Single() = SoilWaterModel.sw

You can also set the values of some APSIM variables:

Dim sw As Single() = SoilWaterModel.sw
sw(1) = 0.29
sw(2) = 0.28
...
SoilWaterModel.sw = sw

 

Data types

It is possible to define custom data types (or structures) that can then be passed from one model to another. Steps to do this:

  1. Define the structure in C:\Apsim\Model\DataTypes\DataTypes.xml
  2. This then gets converted to c# source code by running the batch file: C:\Apsim\Model\DataTypes\DataTypes.bat. For this batch file to work though, you will need FORTRAN compilers. If you’re only interested in having this structure available in your .NET models then you can run the tool manually like this:
    C:\Apsim\Model\processdatatypesinterface.exe DataTypes.xml CSDOTNETDataTypes.macro
    from a command prompt in the datatypes directory. This should create a file called C:\Apsim\Model\DataTypes\DOTNETDataTypes.cs.
  3. The file DOTNETDataTypes.cs gets included in C:\Apsim\Model\CSDotNetComponentInterface\CSDotNetComponentInterface.sln so it will need to be compiled as well.

Example of defining a structure in DataTypes.xml:

<type name="Sow">
   <field name="Cultivar" kind="string" />
   <field name="plants" kind="double" />
   <field name="sowing_depth" kind="double" />
   <field name="row_spacing" kind="double" />
   <field name="SkipRow" kind="double" />
   <field name="SkipPlant" kind="double" />
   <field name="Establishment" kind="string" />
   <field name="crop_class" kind="string" />
   <field name="tiller_no_fertile" kind="string" />
   <field name="Skip" kind="string" />
   <field name="plants_pm" kind="double" />
   <field name="Ratoon" kind="integer4" />
   <field name="sbdur" kind="integer4" />
   <field name="nplh" kind="double" />
   <field name="nh" kind="double" />
   <field name="nplsb" kind="double" />
   <field name="nplds" kind="double" />
</type>

 

Telling the user interface what components your component can be dropped on

To allow the user to drag and drop your models onto a parent model, you will need to edit the xml file (e.g. grossmargin.xml) and add <drop> elements to it indicating which models it can be dropped on to:

<drops>
    <drop>folder</drop>
</drops>

 

Getting Visual Studio to compile your code

From Visual Studio, create a solution, change the location where your binaries are generated to be the APSIM/Model/bin directory. This can be done from the ‘Build’ tab of the solution properties screen. Your .cs files can then be added to your solution.