Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • The MyEngineDLL class is the compiled core engine code (e.g. Fortran).
  • The MyEngineDLLAccess class is responsible for translating the Win32Api from MyEngineDLL to .NET (C#).
  • Calling conventions and exception handling are different for .NET and Fortran. The MyEngineDotNetAccess class ensures that these operations follow the .NET conventions.
  • The MyEngineWrapper class implements the IEngineAccess IEngine interface, which means that it can be accessed by the LinkableEngine class.
  • The MyLinkableEngine class is responsible for the creation of the MyEngineWrapper class and for assigning a reference to this class to a protected field variable in the LinkableEngine class, thus enabling this class to access the MyEngineWrapper class.
     
    More details of these classes are provided in the following sections.
    The OpenMI standard puts a lot of responsibilities on the LinkableComponents. The main idea is that when the GetValues method is invoked the providing component must be able to deliver the requested values so that these apply to the requested time and the requested location. To be able to do this the LinkableComponent may have to interpolate, extrapolate or aggregate both in time and space. These and other things are handled by the LinkableEngine.
     
    The LinkableEngine class includes the following features:
     
  • Buffering: When a model is running as an OpenMI component it may be queried for values that correspond to a time that is before the current time of the model. Most models will only keep values for the current timestep and the previous timestep in memory. It is therefore necessary to store data associated with the OpenMI links in a buffer. The LinkableEngine handles the buffering for you.
  • Temporal interpolation and extrapolation: Most models are only capable of delivering results at times that correspond to their internal timesteps. The LinkableEngine class handles all the temporal operations that are required for LinkableComponents.
  • Spatial operations: The LinkableEngine provides a range of spatial data operations.
  • Link book-keeping: The LinkableEngine handles book-keeping for links added to your component.
  • Event handling: The LinkableEngine sends events that enable an event-listener to monitor the progress of the linked system when running.
     
    More details about how the LinkableEngine works is given in OATC.OpenMI.SDK technical documentation.

...

 The third step is to implement the MyEngineDLLAccess class (Figure 9).

Fig 9. MyEngineDllAccess class
 
Because you are using a C# implementation of OpenMI, your engine needs to be accessible from .NET. In the pattern shown above this is handled in two wrappers, MyEngineDLLAccess and MyEngineDotNetAccess. The MyEngineDLLAccess class will make a one-to-one conversion of all exported functions in the engine core code to public .NET methods. The MyEngineDotNetAccess class will change some of the calling conventions.
The specific implementation of the MyEngineDLLAccess class depends on the compiler you are using. Start by implementing export methods for the Initialize, PerformTimeStep, Finish and Dispose functions.
The code listed below shows an example of such an implementation for the Simple River Fortran engine. Note that this implementation corresponds to a particular Fortran compiler; the syntax may vary between compilers.

Code Block
using System;
 using System.Run-time.InteropServices;
 using System.Text;
 namespace MyOrganisation.OpenMI.MyModel
 {
    public class MyEngineDLLAccess
    {
       \[DLLImport(@'C:\MyEngine\bin\MyEngine.DLL',
          EntryPoint = 'INITIALIZE',
          SetLastError=true,
          ExactSpelling = true,
          CallingConvention=CallingConvention.Cdecl)\]
       public static extern bool Initialize(string filePath, uint length);
       \[DLLImport(@'C:\MyEngine\bin\MyEngine.DLL',
          EntryPoint = 'PERFORMTIMESTEP',
          SetLastError=true,
          ExactSpelling = true,
          CallingConvention=CallingConvention.Cdecl)\]
       public static extern bool PerformTimeStep();
       \[DLLImport(@'C:\MyEngine\bin\MyEngine.DLL',
          EntryPoint = 'FINISH',
          SetLastError=true,
          ExactSpelling = true,
          CallingConvention=CallingConvention.Cdecl)\]
       public static extern bool Finish();
    }
 }

4.4 Step 4: Implementing the MyEngineDotNetAccess

...

Code Block
using System.Text;
 namespace MyOrganisation.OpenMI.MyModel
 {
    public class MyEngineDotNetAccess
    {
       public void Initialize(string filePath)
       {
          if(\!(MyModelDLL.Initialize(filePath, ((uint) filePath.Length))))
          {
             CreateAndThrowException();
          }
       }

        public void PerformTimeStep()
       {
          if(\!(MyModelDLL.PerformTimeStep()))
          {
              CreateAndThrowException();
          }
       }

        public void Finish()
       {
          if(\!(MyModel.Finish()))
          {
             CreateAndThrowException();
          }
       }

        private void CreateAndThrowException()
       {
          int numberOfMessages = 0;
          numberOfMessages = MyModelDLL.GetNumberOfMessages();
          string message = 'Error Message from MyModel ';
          for (int i = 0; i < numberOfMessages; i++)
          {
              int n = i;
              StringBuilder messageFromCore = new StringBuilder(' ');
              MyModelDLL.GetMessage(ref n, messageFromCore,(uint) messageFromCore.Length);
              message \+='; ';
              message \+= messageFromCore.ToString().Trim();
          }
          throw new Exception(message);
      }
 }

4.5 Step 5: Implementing the MyEngineWrapper class

...

The MyEngineWrapper class must implement the ILinkableEngine IEngine interface (Oatc.OpenMI.Sdk.Wrapper.ILinkableEngineIEngine). The easiest way to get started is to make your development environment auto-generate the stub code for this interface.

...


Example code for this is shown below.

Code Block
using System;
 using System.Collections
 namespace MyOrganisation.OpenMI.MyModel
 {
     public class MyEngineWrapper : orgOatc.OpenMI.UtilitiesSdk.Wrapper.IEngine
     {
        private MyEngineDotNetAccess \_myEngine;
        public void Initialize(Hashtable properties)
        {
           \_myEngine = new MyEngineDotNetAccess();
           \_myEngine.Initialize((string)properties\['FilePath'\]);
        }

         public void Finish()
        {
           \_simpleRiverEngine.Finish();
        }
     }
 }

4.6 Step 6: Implementing the MyModelLinkablComponent

...

The MyModelLinkableComponent is the OpenMI-compliant linkable component that is going to be accessed by other models. Implementation of this class is very simple. The example code shown below is the complete implementation for the Simple River model.

Code Block
using System;
 namespace MyOrganisation.OpenMI.MyModel
 {
    public class MyModelOpenMIComponent :
    org.OpenMI.Utilities.Wrapper.LinkableEngine
    {
       protected override void SetEngineApiAccess()
       {
          \_engineApiAccess = new MyEngineWrapper();
       }
    }
 }

This class inherits from the LinkableEngine class. The class creates the EngineWrapper and assigns it to the protected field variable _engineApiAccess.

...

Code Block
//== The org.OpenMI.Utilities.Wrapper.IEngine interface ==
 // \-\- Execution control methods (Inherited from IRunEngine) \-\-
 void Initialize(Hashtable properties);
 bool PerformTimeStep();
 void Finish();
 //-\- Time methods (Inherited from IRunEngine) \-\-
 ITime GetCurrentTime();
 ITime GetInputTime(string QuantityID, string ElementSetID);
 ITimeStamp GetEarliestNeededTime();
 //-\- Data access methods (Inherited from IRunEngine) \-\-
 void SetValues(string QuantityID, string ElementSetID, IValueSet values);
 IValueSet GetValues(string QuantityID, string ElementSetID);
 //-\- Component description methods (Inherited from IRunEngine) \-\-
 double GetMissingValueDefinition();
 string GetComponentID();
 string GetComponentDescription();
 // \-\- Model description methods \-\-
 string GetModelID();
 string GetModelDescription();
 double GetTimeHorizon();
 // \-\- Exchange items \-\-
 int GetInputExchangeItemCount();
 int GetOutputExchangeItemCount();
 org.OpenMI.Backbone GetInputExchangeItem(int exchangeItemIndex);
 org.OpenMI.Backbone GetOutputExchangeItem(int exchangeItemIndex);

...

Code Block
public void Initialize(System.Collections.Hashtable properties)
 {
    \_inputExchangeItems = new ArrayList(); //ArrayList of
    Oatc.OpenMI.Sdk.Backbone.InputExchangeItem objects
    \_outputExchangeItems = new ArrayList(); //ArrayList of
    Oatc.OpenMI.Sdk.Backbone.OutputExchangeItem objects
    // \-\- Create and initialize the engine \-\-
    \_simpleRiverEngine = new SimpleRiverEngineDotNetAccess();
    \_simpleRiverEngine.Initialize((string)properties\['FilePath'\]);
    // \-\- Simulation start time -
    // The start time is obtained from the engine core as a string. This string is
    // passed and converted to a System.DateTime. Then the
    // Oatc.OpenMI.Sdk.DevelopmentSupport.CalendarConverter class is used to convert
    // this time into the ModifiedJulianDay (this is the OpenMI standard time)
    char \[\] delimiter = new char\[\]{'-',' ',':'};
    string\[\] strings = \_simpleRiverEngine.GetSimulationStartDate().Split(delimiter);
    int StartYear = Convert.ToInt32(strings\[0\]);
    int StartMonth = Convert.ToInt32(strings\[1\]);
    int StartDay = Convert.ToInt32(strings\[2\]);
    int StartHour = Convert.ToInt32(strings\[3\]);
    int StartMinute = Convert.ToInt32(strings\[4\]);
    int StartSecond = Convert.ToInt32(strings\[5\]);
    DateTime startDate = new DateTime(StartYear,StartMonth,StartDay,StartHour,StartMinute,StartSecond);
    \_simulationStartTime = Oatc.OpenMI.Sdk.DevelopmentSupport.CalendarConverter.Gregorian2ModifiedJulian(startDate);
    // \-\- Build exchange items \--\-
    Dimension flowDimension = new Dimension();
    Unit flowUnit = new Unit('m3/sec',1,0,'m3/sec'); //The Simple River only uses
    // quantities with the unit m3/sec.
    Quantity flowQuantity = new Quantity(flowUnit,'description','Flow',
    OpenMI.Standard.ValueType.Scalar,flowDimension);
    Quantity inFlowQuantity = new Quantity(flowUnit,'description','InFlow',
    OpenMI.Standard.ValueType.Scalar,flowDimension);
    int numberOfNodes = \_simpleRiverEngine.GetNumberOfNodes();
    for (int i = 0; i < numberOfNodes \-1; i++) //For each branch
    {
       OutputExchangeItem flowFromBranch = new OutputExchangeItem();
       InputExchangeItem inFlowToBranch = new InputExchangeItem();
       // One ElementSet is created for each branch. The ElementID's are
       // Branch:<Branch number>. E.g. 'Branch:3'
       ElementSet branch = new ElementSet('description','Branch:' + i.ToString(),ElementType.XYPolyLine,new SpatialReference('ref'));
       branch.AddElement(new Element('Branch:' + i.ToString()));
       branch.Elements\[0\].AddVertex(new Vertex(_simpleRiverEngine.
       GetXCo-ordinate(i),_simpleRiverEngine.GetYCo-ordinate(i),0));
       branch.Elements\[0\].AddVertex(new Vertex(_simpleRiverEngine.
       GetXCo-ordinate(i+1),_simpleRiverEngine.GetYCo-ordinate(i+1),0));
       flowFromBranch.ElementSet = branch;
       flowFromBranch.Quantity = flowQuantity;
       inFlowToBranch.ElementSet = branch;
       inFlowToBranch.Quantity = inFlowQuantity;
       \_outputExchangeItems.Add(flowFromBranch);
       \_inputExchangeItems.Add(inFlowToBranch);
    }
    for (int i = 0; i < numberOfNodes; i++) //For all nodes
    {
       InputExchangeItem inflowToNode = new InputExchangeItem();
       // Each node is a ID-based ElementSet. The ElementSet ID are
       // Node:<node number>. E.g. 'Node:3'
       ElementSet node = new ElementSet('description','Node:' +
       i.ToString(),ElementType.IDBased,new SpatialReference('ref'));
       node.AddElement(new Element('Node:' + i.ToString()));
       inflowToNode.Quantity = inFlowQuantity;
       inflowToNode.ElementSet = node;
      \_inputExchangeItems.Add(inflowToNode);
    }
    ElementSet Branches = new ElementSet('description','AllBranches',
    ElementType.XYPolyLine,new SpatialReference('ref'));
    for (int i = 0; i < numberOfNodes - 1;i++) //Create an InputExchangeItem that
    // has all branches in one ElementSet
    {
       Element branch = new Element('Branch: ' + i.ToString());
       branch.AddVertex(new Vertex(_simpleRiverEngine.
       GetXCo-ordinate(i),_simpleRiverEngine.GetYCo-ordinate(i),0));
       branch.AddVertex(new Vertex(_simpleRiverEngine.
       GetXCo-ordinate(i+1),_simpleRiverEngine.GetYCo-ordinate(i+1),0));
       Branches.AddElement(branch);
    }
    InputExchangeItem inFlowToBranches = new InputExchangeItem();
    inFlowToBranches.ElementSet = Branches;
    inFlowToBranches.Quantity = inFlowQuantity;
    \_inputExchangeItems.Add(inFlowToBranches);
 }

As you can see from the implementation of the Initialize method, some methods need to be implemented in the MyEngineDotNetAccess class, the MyEngineDLLAccess class and the engine core.

...

Code Block
public org.OpenMI.Standard.IValueSet GetValues(string QuantityID, string ElementSetID)
 {
    double\[\] returnValues;
    Char\[\] separator = new char\[\]{':'};
    if (QuantityID == 'Flow')
    {
       int index = Convert.ToInt32((ElementSetID.Split(separator))\[1\]);
       returnValues = new double\[1\];
       returnValues\[0\] = \_simpleRiverEngine.GetFlow(index);
    }
    else
    {
       throw new Exception('Illegal QuantityID in GetValues method in SimpleRiverEngine');
    }
 }

The branch number is extracted from the ElementSetID and used as an index in the GetValues call to the SimpleRiverDotNetAccess class.

...

Code Block
public OpenMI.Standard.ITime GetCurrentTime()
 {
    double time = \_simulationStartTime + \_simpleRiverEngine.GetCurrentTime() / ((double)(24*3600));
    return new Oatc.OpenMI.Sdk.Backbone.TimeStamp(time);
 }

6. Testing the component

...


Below is the sample test code for the GetModelID method implementation in the Simple River model. Figure 19 shows the NUnit interface.

Code Block
using System;
 using Oatc.OpenMI.Examples.ModelComponents.SimpleRiver.Wrapper;
 using NUnit.Framework;
 namespace org.OpenMITest.Examples.ModelComponents.SimpleRiver.Wrapper
 {
    \[TestFixture\]
    public class SimpleRiverEngineDotNetAccessTest
    {
       \[Test\]
       public void GetModelID()
       {
          SimpleRiverEngineDotNetAccess \_simpleRiverEngineDotNetAccess;
          String \_filePath;
         \_simpleRiverEngineDotNetAccess = new SimpleRiverEngineDotNetAccess();
         \_filePath = 'C:\\SimpleRiver\\UnitTest\\Data\\Rhine';
         \_simpleRiverEngineDotNetAccess.Initialize(_filePath);
         Assert.AreEqual('The river Rhine',
         \_simpleRiverEngineDotNetAccess.GetModelID());
         \_simpleRiverEngineDotNetAccess.Finish();
       }
    }
 }


Fig. 19. NUnit Userinterface with Simple River wrapper classes loaded

...

Code Block
<?XML version='1.0'?>
 <LinkableComponent Type='org.OpenMI.Examples.MC.SimpleRain' Assembly='org.OpenMI.Examples.MC, Version=1.4.0.0, Culture=neutral, PublicKeyToken= 8384b9b46466c568' XMLns='http://www.openmi.org/LinkableComponent.xsd'>
 <Arguments>
    <Argument Key='Data' ReadOnly='true' Value='c:\OpenMI\Examples\Data\SimpleRain.txt' />
 </Arguments>
 </LinkableComponent>

9. The OpenMICompliancyInfo file

...

Code Block
<?xml version="1.0" encoding="UTF-8"?>
 <\!--Sample XML file generated by XMLSpy v2006 rel. 3 sp1 (http://www.altova.com)-->
 <OpenMICompliancyInfo xmlns="http://www.openmi.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openmi.org/schemas/OpenMICompliancyInfo.xsd">
 	<generalSoftwareInfo>
 		<component name="SimpleRiver" version="1.4.0.0">
 			<description>Fortran wrapping sample</description>
 			<url>wiki.OpenMI.org/Oatc.OpenMI.Examples.ModelComponents.SimpleRiver</url>
 		</component>
 		<contactInfo>
 			<supplierName>OATC (OpenMI Association Technical Committee)</supplierName>
 			<contactPerson>Jan Borge Gregersen</contactPerson>
 			<supplierEmail>association@openmi.org</supplierEmail>
 		</contactInfo>
 		<availability component="free" source="available">
 			<comment>Available as part of OATC Software Development Kit</comment>
 		</availability>
 	</generalSoftwareInfo>
 	<supportForOpenMI>
 		<compliancy>
 			<technology>dotNet</technology>
 			<openMIStandardVersion>1.4.0.0</openMIStandardVersion>
 		</compliancy>
 		<exchangeItems>
 			<dataOperations setId="spatial/time/buffering">
 				<dataOperation>Nearest XYPolyLine to XYPoint</dataOperation>
 				<dataOperation>Inverse XYPolyLine to XYPoint</dataOperation>
 				<dataOperation>Weighted Mean XYPolyLine to XYPolyGon</dataOperation>
 				<dataOperation>Weighted Sum XYPolyLine to XYPolyGon</dataOperation>
 				<dataOperation>LinearConversion</dataOperation>
 				<dataOperation>Buffering and temporal extrapolation</dataOperation>
 			</dataOperations>
 			<inputExchangeItem>
 				<quantity>
 					<description>Flow</description>
 					<dimension lenght="3" time="-1"/>
 				</quantity>
 				<elementSet elementType="IDBased">
 					<description>An ElementSet for each branch. Each ElementSet is identified by 'branch-N', where N is the branch number.</description>
 				</elementSet>
 			</inputExchangeItem>
 			<outputExchangeItem>
 				<quantity>
 					<description>Outflow</description>
 					<dimension lenght="3" time="-1"/>
 				</quantity>
 				<elementSet elementType="XYPolyLine">
 					<description>An ElementSet with one element (line segment) for each branch, or an ElementSet with an element for each branch.</description>
 				</elementSet>
 				<dataOperations>spatial/time/buffering</dataOperations>
 			</outputExchangeItem>
 			<outputExchangeItem>
 				<quantity>
 					<description>Outflow</description>
 					<dimension lenght="3" time="-1"/>
 				</quantity>
 				<elementSet elementType="XYPolyLine">
 					<description>An ElementSet with one element (line segment) for each branch, or an ElementSet with an element for each branch.</description>
 				</elementSet>
 				<dataOperation>LinearConversion</dataOperation>
 				<dataOperation>Buffering and temporal extrapolation</dataOperation>
 			</outputExchangeItem>
 			<inputExchangeItem>
 				<quantity>
 					<description>Flow</description>
 					<dimension lenght="3" time="-1"/>
 				</quantity>
 				<elementSet elementType="XYPolyLine">
 					<description>An ElementSet with one element (line segment) for each branch, or an ElementSet with an element for each branch.</description>
 				</elementSet>
 			</inputExchangeItem>
 		</exchangeItems>
 		<optionalInterfaces>
 			<IManageState isSupported="false">
 				<comment>Not implemented for this simple river example</comment>
 			</IManageState>
 			<IDiscreteTimes isSupported="false"/>
 		</optionalInterfaces>
 	</supportForOpenMI>
 </OpenMICompliancyInfo>