OpenPLEXOS refers to a scheme where you can add custom .NET assemblies (libraries) into an input database, and have the PLEXOS engine execute functions inside those assemblies at runtime. By this means you can customize many of the algorithms in PLEXOS, as well as add new algorithms and custom reporting systems.
There are essentially three ways that OpenPLEXOS can interact with custom assemblies, and therefore into the core of PLEXOS:
The 'Load Settlement Model' property, which sets the 'Region Price', has a special option that when activated, causes PLEXOS to call the function 'MyRegion.Price()' in the registered OpenPLEXOS assemblies. This allows to intercept the price calculation and customize the pricing.
In brief, after the appropriate function is implemented (e.g. MyRegion.Price()), in the custom assembly, the OpenPLEXOS will call the function and run the code (this is further explained in section 4.2).
The following calls are made during the course of each simulation phase and step of that phase:
Using the above functions in custom assembly, the call to functions can be intercepted and the customized algorithms can be implemented.
Complete source is provided for the following algorithms:
The source code for the following fully customizable algorithms is also provided (Built-in algorithms that can be customized):
An example of customizing the uplift algorithm will be presented later on in this document.
The solution API allows users to modify the solution database at runtime, allowing the changes to be reflected in all aspects of solution reporting (e.g. Solution file, text file outputs, etc.). This is a powerful feature that exposes the following functions:
These methods can all be accessed through the "solution file wrapper" class: G_oMODEL.SolutionFile
A basic and simple example can be seen here:
Imports EEPHASE
Imports EEUTILITY
Imports EEUTILITY.Enums
Imports EESolutionWrapper
Public Class MyModel
Implements EEPHASE.IOpenModel
Private Shared m_bObjectsCreated As Boolean = False
Private Shared nMembership As Integer
Private Shared nMyReportingPropertyEnum As Integer
Private Shared strMyObjectName = "MyCustomGenerator"
Private Shared strMyReportProperty = "MyReportingProperty"
Private Shared strMySummaryReportProperty = "MySummaryReportingProperty"
Private Shared strNewUnit = "MyUnits"
Private Shared strNewSummaryUnit = "MySummaryUnits"
Private Shared nReportPhases As Integer() = New Integer(3) {SimulationPhaseEnum.LTPlan, SimulationPhaseEnum.PASA,
SimulationPhaseEnum.MTSchedule, SimulationPhaseEnum.STSchedule}
Public Sub BeginInitialize() Implements IOpenModel.BeginInitialize
End Sub
Public Sub AfterInitialize() Implements IOpenModel.AfterInitialize
If (m_bObjectsCreated = False) Then
m_bObjectsCreated = True
G_oMODEL.SolutionFile.AddReportingUnit(strNewUnit)
G_oMODEL.SolutionFile.AddReportingUnit(strNewSummaryUnit)
' Add a Generator object to the solution dataset
nMembership = G_oMODEL.SolutionFile.AddObject(ClassEnum.Generator, strMyObjectName)
' Add a Reporting Property to the solution dataset
nMyReportingPropertyEnum = G_oMODEL.SolutionFile.AddReportingProperty(CollectionEnum.SystemGenerators, strMyReportProperty,
strNewUnit, strSummaryName:=strMySummaryReportProperty, strSummaryUnit:=strNewSummaryUnit)
End If
' Register a calculation function for MyReportProperty
G_oMODEL.SolutionFile.Register_CalculateIntervalData(CollectionEnum.SystemGenerators,
nMembership,
nMyReportingPropertyEnum,
AddressOf MyCustomCalculation,
bWriteFlatFiles:=True,
nReportPhase:=nReportPhases,
bReportSamples:=True,
bReportStatistics:=True)
G_oMODEL.SolutionFile.Register_CalculateSummaryData(CollectionEnum.SystemGenerators,
nMembership,
nMyReportingPropertyEnum,
AddressOf MyCustomCalculation,
SummaryTypeEnum.Sum,
bWriteFlatFiles:=True,
nReportPhase:=nReportPhases)
End Sub
Public Sub AfterOptimize() Implements IOpenModel.AfterOptimize
End Sub
Public Sub AfterProperties() Implements IOpenModel.AfterProperties
End Sub
Public Sub AfterRecordSolution() Implements IOpenModel.AfterRecordSolution
End Sub
Public Sub BeforeOptimize() Implements IOpenModel.BeforeOptimize
End Sub
Public Sub BeforeProperties() Implements IOpenModel.BeforeProperties
End Sub
Public Sub BeforeRecordSolution() Implements IOpenModel.BeforeRecordSolution
End Sub
Public Sub TerminatePhase() Implements IOpenModel.TerminatePhase
End Sub
Public Function EnforceMyConstraints() As Integer Implements IOpenModel.EnforceMyConstraints
End Function
Public Function OnWarning(Message As String) As Boolean Implements IOpenModel.OnWarning
End Function
Public Function MyCustomCalculation() As Double()
Dim oRand As New Random
Return (Enumerable.Repeat(0, Archive.StepPeriodCount)).Select(Function(o) oRand.NextDouble).ToArray
End Function
End Class
Steps in brief:
Note: The above .dll files are located in the PLEXOS installation directory. After adding the .dll to the references, those will be displayed under project "Reference" list (See Figure 3: References list).
Figure 1: Creating a new project
Customizations of these built-in functions can be created by copying the supplied source code into the personalized project, making the required custom changes and overwriting the default OpenPLEXOS assembly.
MyRegion Figure 4: An empty implementation of MyRegion
In the properties window, the "Assembly name" field, shows the name of assembly that will be build later. The default name is the same as the project name created (In this example, it isHelloOpenPLEXOS) (See Figure 5: Properties window). Note, that the project name can be personalized. Click Build ->Build HelloOpenPLEXOS (See Figure 6: Build window).
Build a HelloOpenPLEXOS.dll file and it will be stored in Visual Basic project folder:
...\HelloOpenPLEXOS\HelloOpenPLEXOS\bin\Debug\HelloOpenPLEXOS.dll
Figure 5: Properties windowNOTE: When the assembly file has been coded, it can be stored in any directory.
After the assembly is created, it needs to be registered with PLEXOS. Assembly registration is done per database, i.e. the assemblies need to be registered for every database, in which the OpenPLEXOS runs the custom functions. In this case, we use a simple model named Ex1_Base as an example. Launch the PLEXOS 7 interface and use the "File" -> "Open" to open the model Ex1_Base
The namespace inside the assembly is in which OpenPLEXOS will find the required classes (e.g. MyModel, MyRegion). Note that, by default the namespace of a .NET project is same as the DLL name, but this can be changed via Project settings, or by explicitly defining a namespace in the source file.
Figure 7: SettingsDuring the execution of PLEXOS you will notice that the shipped OpenPLEXOS assembly is loaded at run-time and is reported at the beginning of the onscreen log. PLEXOS will also list any user defined assemblies that the model uses as shown in Figure 10.
Note: Multiple assemblies can be referenced. PLEXOS will call the function from each assembly in the order that they have been added to the Assemblies settings dialog.
Those are the steps required to hook the custom model functions into PLEXOS, however if the intention id to write a custom code for the uplift or pricing algorithms, then it has to be setup explicitly in the model. The "Uplift compatibility" and the "Load Settlement Model" of the "Region" object, need to be set to "Custom". As screenshot was given below.
Load Settlement Modelin property grid shows 8 options, such as Nodal, Uniform, None etc. PLEXOS will not execute MyRegion.Price() Sub in the custom DLL file until the property of Load Settlement Model is set as Custom. When theUplift Enabled is set to Yes and Uplift compatibility to Custom, PLEXOS will execute MyRegion.Uplift() in the custom DLL file. If the Uplift Enabled is set to Yes, Uplift compatibility to CBP or SEM, PLEXOS still will execute MyRegion.Uplift() the custom DLL file and use the result to overwrite the previous uplift. However, if Uplift Enabled is set to No, regardless of the property Uplift Compatibility has, PLEXOS will not execute the custom DLL file.
Figure 13: Config_1
Like writing all applications/programs, it will inevitably require debugging during the development process. There are two ways to debug. The first method is to register the custom assembly to PLEXOS and execute the model in PLEXOS by clicking the "Execute" button from PLEXOS interface; the second method is to register the assembly to PLEXOS model and execute by running the assembly in VB IDE. The second method of debugging is described in this section.
The PLEXOS object model consists of collection of objects that are of a certain class, for example all generators will be grouped into the generator collection, and each generator is of the Generator class.
These next few sections of this document will briefly describe how to access various properties of system memberships, such as SystemGenerators, and non-system collections, such as GeneratorFuels, within the model.
A PLEXOS model internally creates a collection for most of the classes mentioned in (not all are directly required in the model itself, for example the Power station is only required in the compilation of the data) and these are accessible in all functions available in OpenPLEXOS.
A few examples of some of the collections available are:
These collections are essentially arrays of PLEXOS objects, so accessing the properties, for example, of the first generator in the system is as easy as doing: open the watch window (See Figure 22 to know how to get it) while the project is executed and still in run, if you type Generatorsln(1) in the watch window (Figure 23) and then extend it, you can see all its properties (Figure 24).
Figure 22: Open watch windowThe information given is a list of methods, properties or functions, which are available for the generator objects. However the name of the generator accessed would not be known. When using a specific generator, it needs to be located, therefore the very simple example below shows how to iterate through all generators, printing out the name and SRMC of each, and finally when it finds the generator called "Coal_Gen" it writes out the SRMC and Generation of it (for the first period only).
Figure 25: Iterating through all generatorsFigure 26 shows the results of all generators in the model Ex1_Base. The block of code is not necessarily in the AfterInitialize() sub, it can be used at any point. For the built-in subroutines both in MyModel and MyRegion classes, the code can be written to display such as "This is AfterInitialize()" to see the order of execution of each sub as shown in Figure 27.
Figure 27: Simple testEssentially all collections can be iterated in this manner. However this is an iteration through all of the objects in the system collection. The user may actually want to iterate through all fuels that have a membership with a particular generator. These memberships are known as non-system membership (i.e. the parent object is not "System") and is explained in the next section.
All objects can be referenced by their names, as shown below. However, this is not as efficient as using a numeric index.
GeneratorsIn("Coal_Gen")
Non-system memberships are exactly what the name suggests. They are memberships where the parent object is not of a system type. Some of the Typical examples of non-system memberships are:
The process of iterating through the objects involved in the membership is very similar to that of the system collection iteration example. For example the following block of codes lists all generator fuel memberships in the system.
Figure 28: Generator fuel membershipReferring the code, it can be seen that the outer loop is iterating through each generator in the system, and the inner loop is iterating through each fuel that has a membership with the generator.
The membership objects are all in the form of ".m_oObject" where "Object" is replaced with a specific collection, such as fuels, i.e. m_oFuels.
Many objects in PLEXOS also contain objects know as subsets, which are closely related to non-system memberships. Instead of obtaining all fuels in a generator, as described in the previous example, the generator subset in a fuel object can be used to obtain all generators in that fuel. The coded example below (Figure 30) demonstrates how to list all fuel generator membership, which is complement collection of Generator Fuel and Figure 31 shows its results.
Figure 30: Complement collection of Generator FuelThese are the basics of the PLEXOS object model. The source code examples shipped with PLEXOS, provides more useful and in-depth examples.
The previous sections explain how to create assemblies, implement the PLEXOS interface and hook these assemblies up to PLEXOS. This section briefly describes some code snippets that demonstrate a simple custom uplift example (not a real example) with custom reporting.
The requirement for this simple example is to calculate the uplift as being the total generator fixed costs plus a custom physical contract (generation contracts) fixed cost. The custom fixed costs will essentially be a multi-tier physical contract fixed cost, which is not directly supported by PLEXOS. The data detailed in Table1: Physical Contract Fixed Cost Requirement, will be hard coded into the custom assembly however these could easily be read from an external file.
The step number, period, region name, price, SRMC and finally the uplift need to be output as a separate report.
Table 1
Rage (MW) | Fixed Cost $/MW |
---|---|
1 - 50 | 0.1 |
51 - 100 | 2 |
101+ | 3 |
After completing all the prerequisite steps mentioned above, 'creating the assembly', 'configuring the PLEXOS interface' and registering the custom assemblies in the model, the custom assembly coding can be initiated
Imports EEPHASE
Public Class MyModel
Implements EEPHASE.IOpenModel
Public Sub BeginInitialize() Implements IOpenModel.BeginInitialize
End Sub
Public Sub AfterInitialize() Implements IOpenModel.AfterInitialize
'Throw New NotImplementedException()
Console.WriteLine("This is AfterInitialize()")
Dim oFuel As Fuel
For Each oGenerator As Generator In GeneratorsIN
For nCurGeneratorFuel As Integer = 1 To oGenerator.m_oFuels.Count
oFuel = FuelsIN(oGenerator.m_oFuels.Index(nCurGeneratorFuel))
Console.WriteLine("Generator: " & oGenerator.m_strName & " - Fuel:: " & oFuel.m_strName)
Next
Console.WriteLine()
Next
Console.WriteLine()
End Sub
Public Sub AfterOptimize() Implements IOpenModel.AfterOptimize
'Throw New NotImplementedException()
Console.WriteLine("This is AfterOptimize()")
End Sub
Public Sub AfterProperties() Implements IOpenModel.AfterProperties
'Throw New NotImplementedException()
Console.WriteLine("This is AfterProperties()")
End Sub
Public Sub AfterRecordSolution() Implements IOpenModel.AfterRecordSolution
'Throw New NotImplementedException()
Console.WriteLine("This is AfterRecordSolution()")
End Sub
Public Sub BeforeOptimize() Implements IOpenModel.BeforeOptimize
'Throw New NotImplementedException()
Console.WriteLine("This is BeforeOptimize()")
End Sub
Public Sub BeforeProperties() Implements IOpenModel.BeforeProperties
'Throw New NotImplementedException()
Console.WriteLine("This is BeforeProperties()")
End Sub
Public Sub BeforeRecordSolution() Implements IOpenModel.BeforeRecordSolution
'Throw New NotImplementedException()
Console.WriteLine("This is BeforeRecordSolution()")
End Sub
Public Sub TerminatePhase() Implements IOpenModel.TerminatePhase
'Throw New NotImplementedException()
Console.WriteLine("This is TerminatePhase()")
MyRegion.ClearData()
End Sub
Public Function EnforceMyConstraints() As Integer Implements IOpenModel.EnforceMyConstraints
'Throw New NotImplementedException()
Console.WriteLine("This is EnforceMyConstraints()")
Return 0
End Function
Public Function OnWarning(Message As String) As Boolean Implements IOpenModel.OnWarning
'Throw New NotImplementedException()
Console.WriteLine("This is OnWarning(Message As String)")
Return 0
End Function
End Class
Public Class MyRegion
Implements EEPHASE.IOpenRegion
Public Sub Price(nIndex As Integer, dVals As DirtyArray(Of Double)) Implements IOpenRegion.Price
'Throw New NotImplementedException()
Console.WriteLine("This is Price()")
End Sub
Public Sub Uplift(nIndex As Integer, dVals As DirtyArray(Of Double)) Implements IOpenRegion.Uplift
'Throw New NotImplementedException()
Console.WriteLine("This is Uplift()")
InitializeReport()
Dim oRegion As Region = RegionsIN(nIndex)
Dim dGenerationFixedCosts As Double
Dim dContractFixedCosts As Double
With RegionsIN(nIndex)
For nCurPeriod As Integer = 1 To Archive.StepPeriodCount
' stage 1) get the generation costs
For Each oNode As Node In .Nodes
For Each oGenerator As Generator In oNode.Generators
dGenerationFixedCosts += oGenerator.FixedCosts(nCurPeriod)
Next
Next
' stage 2) get the physical contract fixed costs
For Each oPhysicalContract As PhysicalContract In .GenerationContracts
dContractFixedCosts += GetPhysicalContractFixedCosts(oPhysicalContract.Generation(nCurPeriod))
Next
' stage 3) set the uplift value for that period as being
' generator fixed costs plus the physical contract fixed costs
dVals(nCurPeriod) = dGenerationFixedCosts + dContractFixedCosts
dGenerationFixedCosts = 0.0
dContractFixedCosts = 0.0
Next
End With
End Sub
Private GenerationFixedCosts As Double(,) = New Double(,) {{50, 0.1}, {50, 2}, {50, 3}}
Private Function GetPhysicalContractFixedCosts(ByVal dGeneration As Double) As Double
Dim dFixedCost As Double = 0.0
For nCurFixedCost As Integer = 0 To GenerationFixedCosts.GetUpperBound(0)
If (dGeneration < GenerationFixedCosts(nCurFixedCost, 0)) OrElse
nCurFixedCost = GenerationFixedCosts.GetUpperBound(0) Then
dFixedCost += GenerationFixedCosts(nCurFixedCost, 1) * dGeneration
Exit For
Else
dFixedCost += GenerationFixedCosts(nCurFixedCost, 0) * GenerationFixedCosts(nCurFixedCost, 1)
dGeneration -= GenerationFixedCosts(nCurFixedCost, 0)
End If
Next
Return dFixedCost
End Function
Private Shared m_swReport As IO.StreamWriter
Private Sub InitializeReport()
'Output Required:
If m_swReport Is Nothing Then
Dim strReportFile As String = G_oMODEL.OutputPath &
String.Format("\Custom Uplift ( {0} ) Phase {1}.txt", G_oMODEL.Name, G_oMODEL.SimulationPhase.ToString)
m_swReport = New IO.StreamWriter(strReportFile, False)
With m_swReport
.Write("Step" & vbTab)
.Write("Period" & vbTab)
.Write("Region" & vbTab)
.Write("Uplift" & vbTab)
.Write("SRMC" & vbTab)
.Write("Price" & vbTab)
.WriteLine()
End With
End If
End Sub
Private Sub WriteData(ByVal oRegion As Region, ByVal nPeriod As Integer, ByVal dUplift As Double)
With m_swReport
.Write(G_oSTEP.m_nCurStep & vbTab)
.Write(nPeriod & vbTab)
.Write(oRegion.m_strName & vbTab)
.Write(dUplift & vbTab)
.Write(oRegion.ShadowPrice(nPeriod) & vbTab)
.Write((oRegion.ShadowPrice(nPeriod) + dUplift) & vbTab)
.WriteLine()
End With
End Sub
Public Shared Sub ClearData()
If m_swReport IsNot Nothing Then
m_swReport.Close()
m_swReport = Nothing
End If
End Sub
End Class
Firstly, a custom function, GetPhysicalContractFixedCosts() is created to calculate the fixed cost of the physical contract generation.
The uplift calculation is split into three simple stages. The first stage is to calculate the total fixed costs for all generators in the current region, which is as simple as iterating through the generator subset of all nodes in this region. During this process the variable "dGenerationFixedCosts" is continuously updated with the fixed cost of each generator.
The second stage of the code example iterates through all physical contracts in the region, which increments the "dContractFiexedCosts" appropriately. Those are the variables needed for stage three, which is the final uplift calculation. The third stage is as easy as assigning the product of the generation and physical fixed costs to the uplift reference array itself.
Note, that the stages one to three are processed for every period in each step of the simulation.
The final/third stage of this customization is to add the custom reporting. Two functions are created and those will be called within the main Uplift function. The first function is, "InitializeReport()", and simply creates a new file, when the shared file write variable is null (The variable is set to 'null' at the end of every phase) and writes the custom diagnostic column names.
The second function created is called "WriteData()" which has a number of arguments, all of which are data required in the custom report. The uplift calculation plus the various outputs are written to the tab delimited file.
The code snippet that provides full custom uplift with the custom reporting function call added, end at this point. And the following functions are used at the end, terminating the snippet
ClearData() closes the file stream and clear any data that has been allocated.
TerminatePhase() function is a part of the model implementation.