Custom Node Types
There are two ways to define custom node types:
- equation based modeling using
ModelingToolkit.jl
andBlockSystems.jl
- using the
@DynamicNode
macro
Custom Nodes using BlockSystems.jl
PowerDynmaics.jl
provides a node constructor to build nodes from BlockSystem.jl
objects based on voltage setting blocks:
PowerDynamics.IONode
— TypeIONode{T,M} <: AbstractNode
IONode(bp::BlockPara)
IONode(blk::IOBlock, parameters::Dict)
Create an IONode
based on an IOBlock
and a parameter dict.
The block needs to fulfil the folowing interface:
- inputs:
i_r
,i_i
- outputs:
u_r
,u_i
The block gets the flow sum of the connected lines as an input. It should calculate the resulting node voltage as an output (either per DGL or constraint).
The parameters should be provided as a Dict{Symbol,Float}
such as
p = Dict(:a => 1.0,
:b => -π)
Check out the examples/BlockSystems
folder for more examples.
There is another constructor to combine several current injecting blocks to a single node:
PowerDynamics.BusNode
— FunctionBusNode(inj::BlockPara...; name=gensym(:Bus), verbose=false)
Create an IONode
based on several current injectors/draws. Each current injector has to be provided as a BlockPara
object, a composite of an IOBlock
and a parameter dict. Each IOBlock
has to fulfil the interface u_r, u_i ↦ i_r, i_i
.
PowerDynamics.BlockPara
— TypeBlockPara(block, para; strict=true)
Composite type holdes an IOBlock
and a parameter Dict
. Parameter Dict
may be provided with Symbols (:a
) or Symbolic types (blk.a
). The latter will be transformed to Symbols.
If strict=true
the construtor asserts that the given parameters match the internal parameters of the IOBlock
.
Component library
PowerDynamics
has a submodule IOComponents
which contains several predefined building blocks which may be used to build complex nodes.
PowerDynamics.IOComponents.Adder
— FunctionAdder(n=2; name, renamings...)
Returns a simple block which adds n
inputs.
out(t) = a₁(t) + a₂(t) + ...
PowerDynamics.IOComponents.Cart2Polar
— MethodCart2Polar(;name=:c2p, renamings...)
(X, Y) ↦ (mag, arg) transformation
PowerDynamics.IOComponents.Constants
— MethodConstants(constants...)
Returns in IOBlock
with outputs which are directly mapped to values.
julia> blk = PowerDynamics.IOComponents.Constants(:a=>42, :b=>3.14; name=:const)
IOBlock :const with 2 eqs
├ inputs: (empty)
├ outputs: a(t), b(t)
├ istates: (empty)
└ iparams: (empty)
julia> equations(blk)
2-element Vector{Equation}:
a(t) ~ 42
b(t) ~ 3.14
PowerDynamics.IOComponents.DroopControl
— MethodDroopControl(;name, renamings...)
Returns a DroopControl. The name of the system and the names of the vars can be changed with keyword arguments name=:myname, K=:myK, …
.
u = - K*(x - x_ref) + u_ref
+-----------------+
x(t) --| K, x_ref, u_ref |-- u(t)
+-----------------+
IOBlock :##droop# with 1 eqs
├ inputs: x(t)
├ outputs: u(t)
├ istates: (empty)
└ iparams: K, x_ref, u_ref
PowerDynamics.IOComponents.ImpedanceConstraint
— MethodImpedanceConstraint(;name, renamings...)
Returns a Block that calculates complex current for fixed impedance: i = u/Z
i_r = (R u_r + X u_i)/(R² + X²)
i_i = (R u_i - X u_r)/(R² + X²)
+-----+
u_r(t) --| R |-- i_r(t)
u_i(t) --| X |-- i_i(t)
+-----+
PowerDynamics.IOComponents.InversePowerConstraint
— MethodInversePowerConstraint(;name, renamings...)
Returns a Block that calculates complex current for fixed complex power: i = conj(S/u)
i_r = (P u_r + Q u_i)/(u_r² + u_i²)
i_i = (P u_i - Q u_r)/(u_r² + u_i²)
+-----+
u_r(t) --| P |-- i_r(t)
u_i(t) --| Q |-- i_i(t)
+-----+
PowerDynamics.IOComponents.LowPassFilter
— MethodLowPassFilter(;name, renamings...)
Returns a low pass filter. The name of the system and the names of the vars can be changed with keyword arguments name=:myname, τ=:mytau, …
.
out'(t) = 1/τ (in(t) - out(t))
+-----+
input(t) --| τ |-- output(t)
+-----+
IOBlock :##LPF# with 1 eqs
├ inputs: input(t)
├ outputs: output(t)
├ istates: (empty)
└ iparams: τ
PowerDynamics.IOComponents.Polar2Cart
— MethodPolar2Cart(;name=:p2c, renamings...)
(mag, arg) ↦ (X, Y) transformation
PowerDynamics.IOComponents.Power
— MethodPower(;name, renamings...)
Returns a Block which calculates the active and reactive power for a given complex input.
P = uᵣ iᵣ + uᵢ iᵢ
Q = uᵢ iᵣ - uᵣ iᵢ
+-----+
u_r(t) --| |-- P(t)
u_i(t) --| |
i_r(t) --| |
i_i(t) --| |-- Q(t)
+-----+
IOBlock :##power# with 2 eqs
├ inputs: u_i(t), u_r(t), i_i(t), i_r(t)
├ outputs: P(t), Q(t)
├ istates: (empty)
└ iparams: (empty)
PowerDynamics.IOComponents.PowerConstraint
— MethodPowerConstraint(;name, renamings...)
Returns a Block that calculates complex voltage for fixed complex power: u = S/conj(i)
u_r = (P i_r - Q i_i)/(i_r² + i_i²)
u_i = (P i_i + Q i_r)/(i_r² + i_i²)
+-----+
i_r(t) --| P |-- u_r(t)
i_i(t) --| Q |-- u_i(t)
+-----+
PowerDynamics.IOComponents.VoltageSource
— MethodVoltageSource(;name, renamings...)
Returns a VoltageSource Block. Models the complex voltage dynamic as a low pass inspired by Schiffer et. al. for a reference frequency ω and voltage magnitude V.
A = 1/τ ⋅ (V/√(uᵢ² + uᵣ²) - 1)
u_r' = -ω uᵢ + A uᵣ
u_i' = ω uᵣ + A uᵢ
+-----+
ω(t) --| τ |-- u_r(t)
V(t) --| |-- u_i(t)
+-----+
IOBlock :##vsource# with 3 eqs
├ inputs: ω(t), V(t)
├ outputs: u_i(t), u_r(t)
├ istates: A(t)
└ iparams: τ
Custom Nodes using the Node Macro
To define your own Node Types, use the PowerDynamics.@DynamicNode
macro. The new node type will be a subtype of PowerDynamics.AbstractNode
.
PowerDynamics.@DynamicNode
— MacroMacro for creating a new type of dynamic nodes.
Syntax Description:
@DynamicNode MyNewNodeName(Par1, Par2, ...) begin
[MassMatrix definition]
end begin
[all prepratory things that need to be run just once]
end [[x1, dx1], [x2, dx2]] begin
[the actual dynamics equation]
[important to set the output variables]
end
where MyNewNodeName
is the name of the new type of dynamic node, Par1, Par2, ...
are the names of the parameters, x1, x2, ...
the internal variables of the node and dx1, dx2, ...
the corresponding differentials.
In the first block a MassMatrix may be specified. Using the MassMatrix
helper function here is recommended. The whole block can be omitted and the identity matrix I is then used as default.
In the second block, the preparation code that needs to be run only once is inserted. Finally, the third block contains the dynamics description, where it's important that the output variables need to be set. These are du
and the differentials of the internal variables (here dx1, dx2
).
Below are two examples:
@DynamicNode SwingEqParameters(H, P, D, Ω) begin
@assert D > 0 "damping (D) should be >0"
@assert H > 0 "inertia (H) should be >0"
Ω_H = Ω * 2pi / H
end [[ω, dω]] begin
p = real(u * conj(i_c))
dϕ = ω # dϕ is only a temp variable that Julia should optimize out
du = u * im * dϕ
dω = (P - D*ω - p)*Ω_H
end
@DynamicNode SlackAlgebraicParameters(U) begin
MassMatrix() # no masses
end begin
# empty prep block
end [] begin
du = u - U
end
PowerDynamics.MassMatrix
— FunctionMassMatrix(;m_u::Bool = false, m_int = no_internal_masses)
Creates a massmatrix. Calling:
MassMatrix()
creates a mass matrix with all masses turned off.
Keyword Arguments
m_u::Bool=false
: Mass matrix value for the complex voltage u.m_int::Vector{Bool}=Vector{Bool}()
: A vector representing the diagonal of the mass matrix. Specifies the masses for the remaining/internal variables.