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:
- Define the structure in C:\Apsim\Model\DataTypes\DataTypes.xml
- 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. - 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.