Power System Model
JuliaGrid supports the type PowerSystem
to preserve power system data, with the following fields: bus
, branch
, generator
, base
, and model
. The bus
, branch
, and generator
fields hold data related to buses, branches, and generators, respectively. The base
field stores base values for power and voltages, with the default being three-phase power measured in volt-amperes for the base power and line-to-line voltages measured in volts for base voltages. Within the model
field, the ac
and dc
subfields store vectors and matrices pertinent to the power system's topology and parameters, and these are utilized in either the AC or DC framework.
The type PowerSystem
can be created using a function:
JuliaGrid supports three modes for populating the PowerSystem
type: using built-in functions, using HDF5 file format, and using Matpower case files.
It is recommended to use the HDF5 format for large-scale systems. To facilitate this, JuliaGrid has the function:
Upon creation of the PowerSystem
type, users can generate vectors and matrices based on the power system topology and parameters using the following functions:
Once the PowerSystem
type is created, users can add buses, branches, generators, or manage costs associated with the output powers of the generators, using the following functions:
JuliaGrid also provides macros @bus
, @branch
, and @generator
to define templates that aid in creating buses, branches, and generators. These templates help avoid entering the same parameters repeatedly.
Moreover, it is feasible to modify the parameters of buses, branches, and generators. When these functions are executed, all relevant fields within the PowerSystem
type will be automatically updated, encompassing the ac
and dc
fields as well. These functions include:
The functions addBranch!
, addGenerator!
, updateBus!
, updateBranch!
, updateGenerator!
, and cost!
serve a dual purpose. While their primary function is to modify the PowerSystem
type, they are also designed to accept various analysis models like AC or DC power flow models. When feasible, these functions not only modify the PowerSystem
type but also adapt the analysis model, often resulting in improved computational efficiency. Detailed instructions on utilizing this feature can be found in dedicated manuals for specific analyses.
Build Model
The powerSystem
function generates the PowerSystem
type and requires a string-formatted path to either Matpower cases or HDF5 files as input. Alternatively, the PowerSystem
can be created without any initial data by initializing it as empty, allowing the user to construct the power system from scratch.
Matpower File
For example, to create the PowerSystem
type using the Matpower case file for the IEEE 14-bus test case, which is named case14.m
and located in the folder C:\matpower
, the following Julia code can be used:
system = powerSystem("C:/matpower/case14.m")
HDF5 File
In order to use the HDF5 file as input to create the PowerSystem
type, it is necessary to have saved the data using the savePowerSystem
function beforehand. As an example, let us say we saved the power system as case14.h5
in the directory C:\hdf5
. In this case, the following Julia code can be used to construct the PowerSystem
type:
system = powerSystem("C:/hdf5/case14.h5")
It is recommended to load the power system from the HDF5 file to reduce the loading time.
Model from Scratch
Alternatively, the model can be built from scratch using built-in functions, for example:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1, base = 345e3)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05, base = 345e3)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.05)
Internal Unit System
The PowerSystem
type stores all electrical quantities in per-units and radians, except for the base values of power and voltages. The base power value is expressed in volt-amperes, while the base voltages are given in volts.
Change Base Unit Prefixes
The user can retrieve the base power and base voltage values along with their respective units:
julia> system.base.power.value, system.base.power.unit
(1.0e8, "VA")
julia> system.base.voltage.value, system.base.voltage.unit
([345000.0, 345000.0], "V")
By using the @base
macro, users can change the prefixes of the base units. For instance, if users wish to convert base power and base voltage values to megavolt-amperes (MVA) and kilovolts (kV) respectively, they can execute the following macro:
@base(system, MVA, kV)
After executing the macro, the base power and voltage values and their units will be modified accordingly:
julia> system.base.power.value, system.base.power.unit
(100.0, "MVA")
julia> system.base.voltage.value, system.base.voltage.unit
([345.0, 345.0], "kV")
Save Model
Once the PowerSystem
type has been created using one of the methods outlined in Build Model, the data can be stored in the HDF5 file by using the savePowerSystem
function:
savePowerSystem(system; path = "C:/matpower/case14.h5", reference = "IEEE 14-bus test case")
All electrical quantities saved in the HDF5 file are in per-units and radians, except for base values for power and voltages, which are given in volt-amperes and volts. Note that even if the user modifies the base units using the @base
macro, the units will still be saved with the default settings.
Add Bus
The buses can be added both to the loaded power system, or to the one created from scratch. As an illustration, we can initiate the PowerSystem
type and then incorporate two buses by utilizing the addBus!
function:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1, base = 345e3)
addBus!(system; label = "Bus 2", type = 1, angle = -0.034907, base = 345e3)
In this case, we have created two buses where the active power demanded by the consumer at Bus 1
is specified in per-units, which are the same units used to store electrical quantities:
julia> system.bus.demand.active
2-element Vector{Float64}: 0.1 0.0
It is worth noting that the base
keyword is used to specify the base voltages, and its default input unit is in volts (V):
julia> system.base.voltage.value, system.base.voltage.unit
([345000.0, 345000.0], "V")
Also, we have defined the bus voltage angle in radians for Bus 2
as its initial value:
julia> system.bus.voltage.angle
2-element Vector{Float64}: 0.0 -0.034907
We recommend reading the documentation for the addBus!
function, where we have provided a list of all the keywords that can be used.
Customizing Input Units for Keywords
Typically, all keywords associated with electrical quantities are expected to be provided in per-units (pu) and radians (rad) by default, with the exception of base voltages, which should be specified in volts (V). However, users can choose to use different units than the default per-units and radians or modify the prefix of the base voltage unit by using macros such as the following:
@power(MW, MVAr, pu)
@voltage(pu, deg, kV)
This practical example showcases the customization approach. For keywords tied to active powers, the unit is set as megawatts (MW), while reactive powers employ megavolt-amperes reactive (MVAr). Apparent power, on the other hand, employs per-units (pu). As for keywords concerning voltage magnitude, per-units (pu) remain the choice, but voltage angle mandates degrees (deg). Lastly, the input unit for base voltage is elected to be kilovolts (kV).
Now we can create identical two buses as before using new system of units as follows:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 10.0, base = 345.0)
addBus!(system; label = "Bus 2", type = 1, angle = -2.0, base = 345.0)
As can be observed, electrical quantities will continue to be stored in per-units and radians format:
julia> [system.bus.demand.active system.bus.voltage.angle]
2×2 Matrix{Float64}: 0.1 0.0 0.0 -0.0349066
The base voltage values will still be stored in volts (V) since we only changed the input unit prefix, and did not modify the internal unit prefix, as shown below:
julia> system.base.voltage.value, system.base.voltage.unit
([345000.0, 345000.0], "V")
To modify the internal unit prefix, the following macro can be used:
@base(system, VA, kV)
After executing this macro, the base voltage values will be stored in kilovolts (kV):
julia> system.base.voltage.value, system.base.voltage.unit
([345.0, 345.0], "kV")
Add Branch
The branch connecting two buses can be added once those buses are defined, and from
and to
keywords must correspond to labels of those buses. For example:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1)
addBus!(system; label = "Bus 2", type = 1, angle = -0.2)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.12)
Here, we created the branch from Bus 1
to Bus 2
with following parameter:
julia> system.branch.parameter.reactance
1-element Vector{Float64}: 0.12
It is recommended to consult the documentation for the addBranch!
function, where we have provided a list of all the keywords that can be used.
Customizing Input Units for Keywords
To use units other than per-units (pu) and radians (rad), macros can be employed to change the input units. For example, if there is a need to use ohms (Ω), the macros below can be employed:
@parameter(Ω, pu)
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1)
addBus!(system; label = "Bus 2", type = 1, angle = -0.2)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 22.8528)
Still, all electrical quantities are stored in per-units, and the same branch as before is created:
julia> system.branch.parameter.reactance
1-element Vector{Float64}: 0.11999999999999998
It is important to note that, when working with impedance and admittance values in ohms (Ω) and siemens (S) that are related to a transformer, the assignment must be based on the primary side of the transformer.
Add Generator
The generator connected to a bus can be added once the bus is defined. Each generator must have a unique label, and the bus
keyword should correspond to the unique label of the bus it is connected to. For instance:
system = powerSystem()
addBus!(system; label = "Bus 1")
addBus!(system; label = "Bus 2")
addGenerator!(system; label = "Generator 1", bus = "Bus 2", active = 0.5, reactive = 0.1)
In the above code, we add the generator to the Bus 2
, with active and reactive power outputs set to:
julia> system.generator.output.active, system.generator.output.reactive
([0.5], [0.1])
Similar to buses and branches, the input units can be changed to units other than per-units using different macros.
It is recommended to refer to the documentation for the addGenerator!
function, where we have provided a list of all the keywords that can be used.
Add Templates
The functions addBus!
, addBranch!
, and addGenerator!
are used to add bus, branch, and generator to the power system, respectively. If certain keywords are not specified, default values are assigned to some parameters.
Default Keyword Values
Regarding the addBus!
function, the bus type is automatically configured as a demand bus with type = 1
. The initial bus voltage magnitude is set to magnitude = 1.0
per-unit, while the base voltage is established as base = 138e3
volts. Additionally, the minimum and maximum bus voltage magnitudes are set to minMagnitude = 0.9
per-unit and maxMagnitude = 1.1
per-unit, respectively.
Transitioning to the addBranch!
function, the default operational status is status = 1
, indicating that the branch is in-service. The off-nominal turns ratio for the transformer is specified as turnsRatio = 1.0
, and the phase shift angle is set to shiftAngle = 0.0
, collectively defining the line configuration with these standard settings. The flow rating is also configured as type = 1
. Moreover, the minimum and maximum voltage angle differences between the from-bus and to-bus ends are set to minDiffAngle = -2pi
and maxDiffAngle = 2pi
, respectively.
Similarly, the addGenerator!
function designates an operational generator by employing status = 1
, and it sets magnitude = 1.0
per-unit, denoting the desired voltage magnitude setpoint.
The remaining parameters are initialized with default values of zero.
Change Default Keyword Values
In JuliaGrid, users have the flexibility to adjust default values and assign customized values using the @bus
, @branch
, and @generator
macros. These macros create bus, branch, and generator templates that are used every time the addBus!
, addBranch!
, and addGenerator!
functions are called. For instance, the code block shows an example of creating bus, branch, and generator templates with customized default values:
system = powerSystem()
@bus(type = 2, active = 0.1)
addBus!(system; label = "Bus 1")
addBus!(system; label = "Bus 2", type = 1, active = 0.5)
@branch(reactance = 0.12)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2")
addBranch!(system; label = "Branch 2", from = "Bus 1", to = "Bus 2", reactance = 0.06)
@generator(magnitude = 1.1)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 0.6)
addGenerator!(system; label = "Generator 2", bus = "Bus 1", active = 0.2)
This code example involves two uses of the addBus!
and addBranch!
functions. In the first use, the functions rely on the default values set by the templates created with the @bus
and @branch
macros. In contrast, the second use passes specific values that match the keywords used in the templates. As a result, the templates are ignored:
julia> system.bus.layout.type
2-element Vector{Int8}: 2 1
julia> system.bus.demand.active
2-element Vector{Float64}: 0.1 0.5
julia> system.branch.parameter.reactance
2-element Vector{Float64}: 0.12 0.06
In the given example, the @generator
macro is utilized instead of repeatedly specifying the magnitude
keyword in the addGenerator!
function. This macro creates a generator template with a default value for magnitude
, which is automatically applied every time the addGenerator!
function is called. Therefore, it eliminates the requirement to set the magnitude value for each individual generator:
julia> system.generator.voltage.magnitude
2-element Vector{Float64}: 1.1 1.1
Customizing Input Units for Keywords
Templates can also be defined using a custom unit system, for example:
system = powerSystem()
@power(MW, MVAr, MVA)
@bus(active = 100, reactive = 200)
addBus!(system; label = "Bus 1")
@power(pu, pu, pu)
addBus!(system; label = "Bus 2", active = 0.5)
In this example, we create the bus template and one bus using SI power units, and then we switch to per-units and add the second bus. It is important to note that once the template is defined in any unit system, it remains valid regardless of subsequent unit system changes. The resulting power values are:
julia> system.bus.demand.active
2-element Vector{Float64}: 1.0 0.5
julia> system.bus.demand.reactive
2-element Vector{Float64}: 2.0 2.0
Thus, JuliaGrid automatically tracks the unit system used to create templates and provides the appropriate conversion to per-units and radians. Even if the user switches to a different unit system later on, the previously defined template will still be valid.
Multiple Templates
In the case of calling the @bus
, @branch
, or @generator
macros multiple times, the provided keywords and values will be combined into a single template for the corresponding component (bus, branch, or generator), which will be used for generating the component.
Reset Templates
To reset the bus, branch, and generator templates to their default settings, users can utilize the following macros:
@default(bus)
@default(branch)
@default(generator)
Additionally, users can reset all templates using the macro:
@default(template)
Labels
As we have shown, JuliaGrid mandates a distinctive label for every bus, branch, or generator. These labels are stored in ordered dictionaries, functioning as pairs of strings and integers. The string signifies the exclusive label for the specific component, whereas the integer maintains an internal numbering of buses, branches, or generators.
String labels improve readability, but in larger models, the overhead from using strings can become substantial. To reduce memory usage, users can configure ordered dictionaries to accept and store integers as labels:
@labels(Integer)
Integer-Based Labeling
Let us take a look at the following illustration:
@labels(Integer)
system = powerSystem()
addBus!(system; label = 1, type = 3, active = 0.1)
addBus!(system; label = 2, type = 1, angle = -0.2)
addBranch!(system; label = 1, from = 1, to = 2, reactance = 0.12)
addGenerator!(system; label = 1, bus = 2, active = 0.5, reactive = 0.1)
In this example, we use the macro @labels
to specify that labels will be stored as integers. It is essential to run this macro; otherwise, even if integers are used in subsequent functions, they will be stored as strings.
Here, two buses are created with labels 1
and 2
. A branch connects these two buses, assigned a unique label of 1
. Finally, a generator is connected to bus 2
, with its own label set to 1
.
Automated Labeling
Users also possess the option to omit the label
keyword, allowing JuliaGrid to independently allocate unique labels for buses, branches, or generators. In such instances, JuliaGrid employs an ordered set of incremental integers for labeling components. To illustrate, consider the subsequent example:
system = powerSystem()
addBus!(system; type = 3, active = 0.1)
addBus!(system; type = 1, angle = -0.2)
addBranch!(system; from = 1, to = 2, reactance = 0.12)
addGenerator!(system; bus = 2, active = 0.5, reactive = 0.1)
This example models the same power system as before. In the previous case, we manually assigned labels using incremental integers. Here, we rely on the automatic labeling behavior, but since the macro @labels
is not used, the labels will be stored as strings.
Automated Labeling Using Templates
Additionally, users have the ability to generate labels through templates and employ the symbol ?
to insert an incremental set of integers at any location. For instance:
system = powerSystem()
@bus(label = "Bus ? HV")
addBus!(system; type = 3, active = 0.1)
addBus!(system; type = 1, angle = -0.2)
@branch(label = "Branch ?")
addBranch!(system; from = "Bus 1 HV", to = "Bus 2 HV", reactance = 0.12)
@generator(label = "Generator ?")
addGenerator!(system; bus = "Bus 2 HV", active = 0.5, reactive = 0.1)
In this example, two buses are generated and labeled as Bus 1 HV
and Bus 2 HV
, along with one branch and one generator labeled as Branch 1
and Generator 1
, respectively.
Retrieving Labels
Finally, we will outline how users can retrieve stored labels. Let us consider the following power system creation:
system = powerSystem()
addBus!(system; label = "Bus 2")
addBus!(system; label = "Bus 1")
addBus!(system; label = "Bus 3")
addBranch!(system; label = "Branch 2", from = "Bus 2", to = "Bus 1", reactance = 0.8)
addBranch!(system; label = "Branch 1", from = "Bus 2", to = "Bus 3", reactance = 0.5)
addGenerator!(system; label = "Generator 2", bus = "Bus 1")
addGenerator!(system; label = "Generator 1", bus = "Bus 3")
For instance, the bus labels can be accessed using the variable:
julia> system.bus.label
OrderedCollections.OrderedDict{String, Int64} with 3 entries: "Bus 2" => 1 "Bus 1" => 2 "Bus 3" => 3
If the objective is to obtain only labels, users can utilize the following:
julia> label = collect(keys(system.bus.label))
3-element Vector{String}: "Bus 2" "Bus 1" "Bus 3"
This approach can also be extended to branch and generator labels by making use of the variables present within the PowerSystem
type, namely system.branch.label
or system.generator.label
.
Moreover, the from
and to
keywords associated with branches are stored based on internally assigned numerical values linked to bus labels. These values are stored in variables:
julia> [system.branch.layout.from system.branch.layout.to]
2×2 Matrix{Int64}: 1 2 1 3
To recover the original from
and to
labels, we can utilize:
julia> [label[system.branch.layout.from] label[system.branch.layout.to]]
2×2 Matrix{String}: "Bus 2" "Bus 1" "Bus 2" "Bus 3"
Similarly, the bus
keywords related to generators are saved based on internally assigned numerical values corresponding to bus labels and can be accessed using:
julia> system.generator.layout.bus
2-element Vector{Int64}: 2 3
To recover the original bus
labels, we can utilize:
julia> label[system.generator.layout.bus]
2-element Vector{String}: "Bus 1" "Bus 3"
JuliaGrid offers the capability to print labels alongside various types of data, such as power system parameters, voltages, powers, currents, or constraints used in optimal power flow analyses. For instance:
julia> print(system.branch.label, system.branch.parameter.reactance)
Branch 2: 0.8 Branch 1: 0.5
Loading and Saving Labels
When a user loads a power system from a Matpower file, the default behavior is to store labels as strings. However, this can be overridden by using the @labels
macro to store labels as integers.
When saving the power system to an HDF5 file, the label type (strings or integers) will match the type chosen during system setup. Likewise, when loading data from an HDF5 file, the label type will be preserved as saved, regardless of what is set by the @labels
macro.
AC and DC Model
When we constructed the power system, we can create an AC and/or DC model, which include vectors and matrices related to the power system's topology and parameters. The following code snippet demonstrates this:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05)
addBus!(system; label = "Bus 3", type = 1, susceptance = 0.05)
addBranch!(system; from = "Bus 1", to = "Bus 2", reactance = 0.12, shiftAngle = 0.1745)
addBranch!(system; from = "Bus 2", to = "Bus 3", resistance = 0.008, reactance = 0.05)
acModel!(system)
dcModel!(system)
In many instances throughout the JuliaGrid documentation, we explicitly mention these functions by their names, although it is not mandatory. If a user begins any of the various AC or DC analyses without having previously established the AC or DC model using the acModel!
or dcModel!
function, the respective function for setting the analysis will automatically create the AC or DC model.
The nodal matrices are one of the components of both the AC and DC models and are stored in the variables:
julia> system.model.ac.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries: 0.0-8.33333im -1.4468+8.20678im ⋅ 1.4468+8.20678im 3.12012-27.8341im -3.12012+19.5008im ⋅ -3.12012+19.5008im 3.12012-19.4508im
julia> system.model.dc.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 7 stored entries: 8.33333 -8.33333 ⋅ -8.33333 28.3333 -20.0 ⋅ -20.0 20.0
The AC model is used for performing AC power flow, AC optimal power flow, AC state estimation, or state estimation with PMUs, whereas the DC model is essential for various DC or linear analyses. Consequently, once these models are developed, they can be applied to various types of simulations. We recommend that the reader refers to the tutorial on AC and DC models.
New Branch Triggers Model Update
We can execute the acModel!
and dcModel!
functions after defining the final number of buses, and each new branch added will trigger an update of the AC and DC matrices and vectors. Here is an example:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1)
addBus!(system; label = "Bus 2", type = 1, reactive = 0.05)
addBus!(system; label = "Bus 3", type = 1, susceptance = 0.05)
acModel!(system)
dcModel!(system)
addBranch!(system; from = "Bus 1", to = "Bus 2", reactance = 0.12, shiftAngle = 0.1745)
addBranch!(system; from = "Bus 2", to = "Bus 3", resistance = 0.008, reactance = 0.05)
For example, the nodal matrix in the DC framework has the same values as before:
julia> system.model.dc.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 7 stored entries: 8.33333 -8.33333 ⋅ -8.33333 28.3333 -20.0 ⋅ -20.0 20.0
It is not fully recommended to create AC and DC models before adding a large number of branches if the execution time of functions is important. Instead, triggering updates to the AC and DC models using the addBranch!
function is useful for power systems that require the addition of several branches. This update avoids the need to recreate vectors and matrices from scratch.
New Bus Triggers Model Erasure
The AC and DC models must be defined once a finite number of buses has been defined, otherwise, adding a new bus will delete them. For example, if we attempt to add a new bus to the PowerSystem
type that was previously created, the current AC and DC models will be completely erased:
julia> addBus!(system; label = "Bus 4", type = 2)
[ Info: The AC model has been completely erased. [ Info: The DC model has been completely erased.
julia> system.model.ac.nodalMatrix
0×0 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 0 stored entries
julia> system.model.dc.nodalMatrix
0×0 SparseArrays.SparseMatrixCSC{Float64, Int64} with 0 stored entries
Update Bus
Once a bus has been added to the PowerSystem
type, users have the flexibility to modify all parameters defined within the addBus!
function. This means that when the updateBus!
function is used, the PowerSystem
type within AC and DC models that have been created is updated. This eliminates the need to recreate the AC and DC models from scratch.
To illustrate, let us consider the following power system:
system = powerSystem()
addBus!(system; label = "Bus 1", type = 3, active = 0.1, conductance = 0.01)
addBus!(system; label = "Bus 2", type = 2, reactive = 0.05)
addBus!(system; label = "Bus 3", type = 1, susceptance = 0.05)
addBranch!(system; label = "Branch 1", from = "Bus 1", to = "Bus 2", reactance = 0.12)
addBranch!(system; label = "Branch 2", from = "Bus 2", to = "Bus 3", reactance = 0.05)
addGenerator!(system; label = "Generator 1", bus = "Bus 1", active = 0.5)
addGenerator!(system; label = "Generator 2", bus = "Bus 1", active = 0.2)
acModel!(system)
dcModel!(system)
For instance, the nodal matrix in the AC framework has the following form:
julia> system.model.ac.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries: 0.01-8.33333im -0.0+8.33333im ⋅ 0.0+8.33333im 0.0-28.3333im -0.0+20.0im ⋅ 0.0+20.0im 0.0-19.95im
Now, let us add a shunt element to Bus 2
:
updateBus!(system; label = "Bus 2", conductance = 0.4, susceptance = 0.5)
As we can observe, executing the function triggers an update of the AC nodal matrix:
julia> system.model.ac.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries: 0.01-8.33333im -0.0+8.33333im ⋅ 0.0+8.33333im 0.4-27.8333im -0.0+20.0im ⋅ 0.0+20.0im 0.0-19.95im
Update Branch
Once a branch has been added to the PowerSystem
type, users have the flexibility to modify all parameters defined within the addBranch!
function. This means that when the updateBranch!
function is used, the PowerSystem
type within AC and DC models that have been created is updated. This eliminates the need to recreate the AC and DC models from scratch.
To illustrate, let us continue with the previous example and modify the parameters of Branch 1
as follows:
updateBranch!(system; label = "Branch 1", resistance = 0.012, reactance = 0.3)
We can observe the update in the AC nodal matrix:
julia> system.model.ac.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries: 0.14312-3.32801im -0.13312+3.32801im ⋅ -0.13312+3.32801im 0.53312-22.828im -0.0+20.0im ⋅ 0.0+20.0im 0.0-19.95im
Next, let us switch the status of Branch 2
from in-service to out-of-service:
updateBranch!(system; label = "Branch 2", status = 0)
As before, the updated AC nodal matrix takes the following form:
julia> system.model.ac.nodalMatrix
3×3 SparseArrays.SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries: 0.14312-3.32801im -0.13312+3.32801im ⋅ -0.13312+3.32801im 0.53312-2.82801im 0.0+0.0im ⋅ 0.0+0.0im 0.0+0.05im
Drop Zeros
After the last execution of the updateBranch!
function, the nodal matrices will contain zeros, as demonstrated in the code example. If needed, the user can remove these zeros using the dropZeros!
function, as shown below:
dropZeros!(system.model.ac)
It is worth mentioning that in simulations conducted with the JuliaGrid package, the precision of the outcomes remains unaffected even if zero entries are retained. However, we recommend users utilize this function instead of dropzeros!
from the SuiteSparse package to ensure seamless functioning of all JuliaGrid functionalities.
Update Generator
Finally, users can update all generator parameters defined within the addGenerator!
function using the updateGenerator!
function. The execution of this function will affect all variables within the PowerSystem
type.
In short, in addition to the generator
field, JuliaGrid also retains variables associated with generators within the bus
field. As an example, let us examine one of these variables and its values derived from a previous example:
julia> system.bus.supply.active
3-element Vector{Float64}: 0.7 0.0 0.0
Next, we will change the active output power of Generator 1
:
updateGenerator!(system; label = "Generator 1", active = 0.9)
As we can see, executing the function triggers an update of the observed variable:
julia> system.bus.supply.active
3-element Vector{Float64}: 1.1 0.0 0.0
Hence, this function ensures the adjustment of generator parameters and updates all fields of the PowerSystem
type affected by them.
Add and Update Costs
The cost!
function is responsible for adding and updating costs associated with the active or reactive power produced by the corresponding generator. These costs are added only if the corresponding generator is defined.
To start, let us create an example of a power system using the following code:
system = powerSystem()
addBus!(system; label = "Bus 1")
addBus!(system; label = "Bus 2")
addGenerator!(system; label = "Generator 1", bus = "Bus 2")
Polynomial Cost
Let us define a quadratic polynomial cost function for the active power produced by the Generator 1
:
cost!(system; label = "Generator 1", active = 2, polynomial = [1100.0; 500.0; 150.0])
In essence, what we have accomplished is the establishment of a cost function depicted as $f(P_{\text{g}1}) = 1100 P_{\text{g}1}^2 + 500 P_{\text{g}1} + 150$ through the code provided. In general, when constructing a polynomial cost function, the coefficients must be ordered from the highest degree to the lowest.
The default input units are in per-units (pu), with coefficients of the cost function having units of currency/pu²-hr for 1100, currency/pu-hr for 500, and currency/hr for 150. Therefore, the coefficients are stored exactly as entered:
julia> system.generator.cost.active.polynomial[1]
3-element Vector{Float64}: 1100.0 500.0 150.0
By setting active = 2
within the function, we express our intent to specify the active power cost using the active
key. By using a value of 2
, we signify our preference for employing a polynomial cost model for the associated generator. This flexibility is neccessary when we have also previously defined a piecewise linear cost function for the same generator. In such cases, we can set active = 1
to utilize the piecewise linear cost function to represent the cost of the corresponding generators. Thus, we retain the freedom to choose between these two cost functions according to the requirements of our simulation. Additionally, users have the option to define both piecewise and polynomial costs within a single function call, further enhancing the versatility of the implementation.
Piecewise Linear Cost
We can also create a piecewise linear cost function, for example, let us create the reactive power cost function for the same generator using the following code:
cost!(system; label = "Generator 1", reactive = 1, piecewise = [0.11 12.3; 0.15 16.8])
The first column denotes the generator's output reactive powers in per-units, while the second column specifies the corresponding costs for the specified reactive power in currency/hr. Thus, the data is stored exactly as entered:
julia> system.generator.cost.reactive.piecewise[1]
2×2 Matrix{Float64}: 0.11 12.3 0.15 16.8
Customizing Input Units for Keywords
Changing input units from per-units (pu) can be particularly useful since cost functions are usually related to SI units. Let us set active powers in megawatts (MW) and reactive powers in megavolt-amperes reactive (MVAr):
@power(MW, MVAr, pu)
Now, we can add the quadratic polynomial function using megawatts:
cost!(system; label = "Generator 1", active = 2, polynomial = [0.11; 5.0; 150.0])
After inspecting the resulting cost data, we can see that it is the same as before:
julia> system.generator.cost.active.polynomial[1]
3-element Vector{Float64}: 1100.0 500.0 150.0
Similarly, we can define the linear piecewise cost using megavolt-amperes reactive:
cost!(system; label = "Generator 1", reactive = 1, piecewise = [11.0 12.3; 15.0 16.8])
Upon inspection, we can see that the stored data is the same as before:
julia> system.generator.cost.reactive.piecewise[1]
2×2 Matrix{Float64}: 0.11 12.3 0.15 16.8
The cost!
function not only adds costs but also allows users to update previously defined cost functions. This functionality is particularly valuable in optimal power flow analyses, as it allows users to modify generator power costs without the need to recreate models from scratch.