GithubHelp home page GithubHelp logo

juliaenergy / powerdynamics.jl Goto Github PK

View Code? Open in Web Editor NEW
101.0 9.0 31.0 2.53 MB

Package for dynamical modeling of power grids

Home Page: https://juliaenergy.github.io/PowerDynamics.jl/latest/

License: GNU General Public License v3.0

Makefile 0.26% Julia 99.74%

powerdynamics.jl's Introduction

codecov Stable Docs Dev Docs

PowerDynamics.jl

PowerDynamics.jl: An Open-Source Framework Written in Julia for Dynamic Power Grid Modeling and Analysis. Please check out the Docs.

Citation

If you use PowerDynamics.jl in your research publications, please cite our paper.

@article{PowerDynamics2022,
  title={PowerDynamics.jl--An experimentally validated open-source package for the dynamical analysis of power grids},
  author={Plietzsch, Anton and Kogler, Raphael and Auer, Sabine and Merino, Julia and Gil-de-Muro, Asier and Li{\ss}e, Jan and Vogel, Christina and Hellmann, Frank},
  journal = {SoftwareX},
  volume = {17},
  pages = {100861},
  year = {2022},
  publisher={Elsevier}
}

Funding

Development of PowerDynamics.jl was in part funded by the German Federal Ministry for Economic Affairs and Climate Action

powerdynamics.jl's People

Contributors

annabuettner avatar antonplietzsch avatar byronbest avatar fhell avatar fredrikekre avatar github-actions[bot] avatar hexaeder avatar janlisse avatar jw3126 avatar lindnemi avatar logankilpatrick avatar luap-pik avatar mortenpi avatar odow avatar sabineauer avatar timziebart avatar wouterver avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

powerdynamics.jl's Issues

Adding general forms of control

I want to document this approach to implement a general (e.g. non-local) form of control by defining a custom PowerGrid type.

The point is that the vertex (and edge) functions in NetworkDynamics cannot access the state vector for other components. Hence, the controller here is a wrapper around the rhs that has access to
the full state vector u. The result is then passed via the parameter field.

This functionality can be easily added, although it will probably deprecate when NetworkDynamics offers multilayer support (?).

In Any case, I think it's a good idea to document the process of defining custom grids in the official docs.

struct ControlledPowerGrid <: AbstractPowerGrid
    graph:: G where G <: AbstractGraph
    nodes
    lines
    controller # oop function
end

# Constructor
function ControlledPowerGrid(control, pg::PowerGrid, args...)
    ControlledPowerGrid(pg.graph, pg.nodes, pg.lines, (u, p, t) -> control(u, p, t, args...))
end

#rhs
function rhs(cpg::ControlledPowerGrid)
    open_loop_dyn = PowerGrid(cpg.graph, cpg.nodes, cpg.lines) |> rhs
    function cpg_rhs!(du, u, p, t)
        # Get controller state
        control_output = cpg.controller(u, p, t)
        # Calculate the derivatives, passing the controller state through
        open_loop_dyn(du, u, control_output, t)
    end
    ODEFunction{true}(cpg_rhs!, mass_matrix=open_loop_dyn.mass_matrix, syms=open_loop_dyn.syms)
end

For many methods, e.g. systemsize, the type doesn't matter as long as we keep the same fields as the PowerGrid type. Hence, it might make sense to dispatch on an AbstractPowerGrid to allow more flexibility without manually extending lot's of methods.

Current and power nodes

I wanted to document some thoughts of something we might want to implement going forward. Right now we make the modelling assumption that every node specifies a voltage, and the current then adapts to the voltages. We could also allow for nodes to specify currents and possibly powers.

A way to implement this would be to add a Holy-Trait to each node type that specifies whether the x[1:2] and dx[1:2] is a voltage or a current. Our lines then need to be made compatible with that. In practice this might mean that Static line has to have three implementations: V to V, V to I and I to I. When we assemble the grid we have to pick the right one based on the node type.

As an example, the V to V static line essentially looks like this right now:

I = Y * V

The I to I line would calculate the voltage as a function of the current:

V = Y^-1 * I

Update docs with more detail

  • explain parameters for line types
  • self admittance of nodes is not documented, yet
  • explain functions, e.g inc() for perturbations
  • update docs for wind turbine generator and renewable generator with inertial response

planned renaming

Here is a list of suggested / planned renamings that will be executed before publication. (Result of discussion on gitlab.)

  • file NetwirkRHSs.jl to NetworkRHSs.jl
  • SystemSize --> systemsizeof
  • Nodes --> nodesof
  • AdmittanceLaplacian --> nodeadmittancesof / NodeAdmittance
  • LY --> NY
  • NetworkRHS --> rhsof
  • AbstractNodeDynamics.rhs/root --> rhsof(AbstractNodeDynamics)
  • instead of constructors, use ...of, e.g. GridDynamics to gridDynamicsOf

write a simple benchmarking system

write a basic benchmark system for

  • execution speed of solving a single trajectory
  • execution of Monte Carlo Samples (later)
  • parameter space scanning, or LSolve / NLsolve for variable S or P in a operation point search

StackOverflowError for VoltageDependentLoad

I get a StackoverFlowError for a PowerPerturbation simulation of a VoltageDependentLoad. As a minimal example I take the same setup as in the test file and compare the same simulation for a PQAlgebraic and the VoltageDependentLoad with parameters A=B=0. For the PQAlgebraic the simulation succeeds, for the VoltageDependentLoad I get the Stack Overflow.

using PowerDynamics
##
P, Q = rand(2)
U = 1.
A = 0.
B = 0.
##
slack = SlackAlgebraic(U = U)
line = StaticLine(;from = 1, to = 2, Y = 10 / (0.1152 + im * 0.0458))
load = VoltageDependentLoad(P = P, Q = Q, U = U, A = A, B = B)
pq = PQAlgebraic(P = P, Q = Q)
##
pg_pq = PowerGrid([slack, pq], [line,])
pg_load = PowerGrid([slack, load], [line,])
##
op_load = find_operationpoint(pg_load)
op_pq = find_operationpoint(pg_pq)
##
timespan = (0., 10.)
pd = PowerPerturbation(
    node = 2,
    fault_power = 0.,
    tspan_fault = (5.,6.),
    var = :P)
##
result_pq = simulate(pd, op_pq, timespan)
result_load = simulate(pd, op_load, timespan)

Tests are Failing, mostly with diagonal matrixes

When` I run runtests.jl 13 test fail like...

Test threw exception Expression: CSI_min_vertex.mass_matrix |> diag == [0, 0] ArgumentError: use diagm instead of diag to construct a diagonal matrix Stacktrace: [1] diag(::Array{Int64,1}) at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/LinearAlgebra/src/generic.jl:425 [2] |>(::Array{Int64,1}, ::typeof(diag)) at ./operators.jl:834 [3] top-level scope at /home/*/PowerDynamics.jl/test/nodes/CurrentSourceInverterMinimal.jl:11 [4] top-level scope at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115 [5] top-level scope at /home/*/PowerDynamics.jl/test/nodes/CurrentSourceInverterMinimal.jl:6

Is this a setup issue or a bug that needs to be fixed?

Fix docs

After the 2.0 release the documentation needs an update.
We should also fix the warnings that are thrown during doc generation with Documenter.jl.

Compatibilities

I think the Project.toml file should be reviewed. Right now using PowerDynamics 2.3.0 is not trivial and Julia does not efficiently tell you what the problem is.

What is the problem:
If you have any of the packages in the [compat] section of the Project.toml file already manually installed, and the version of the installed package is not listed in the compat section, then the PowerDynamics version with that Project.toml file cannot be installed.

For example if I have RecipesBase 0.8.0 installed in my Julia environment, then I cannot install at the same time PowerDynamics 2.3.0 . Even more troubling is that if I have some Package that requires RecipesBase 0.8.0 I still cannot use PowerDynamics 2.3.0 and moreover Julia doesn't tell me which other package is causing the trouble. This is surely in my opinion some kind of bad behavior of Julia, but still this package should cope with that.

A solution could be to extend the compatibilities to more versions of the single packages. Since it is mainly the packages with versions under "v1.0.0" causing the problem ( "^v1.0.0" implies compatibility with all the versions up to 1.9.9 and "^v0.1.0" implies compatibility only up to 0.1.9 and 0.x.x packages get often changed to formally incompatible versions, even though there happened no major change.), maybe more versions should be allowed for those.
Moreover, I think that when submitting a new version it may be important to try to include also the latest version (if really compatible) of the packages in the compatibilities.

The packages that are outdated in the compat section are RecipesBase (now at 0.8), Lazy (now at 0.15), Setfield (now at 0.6) , StaticArrays (now at 0.12).

Including multiple versions is possible like this: RecipesBase = "0.7, 0.8".

Old PD versions

I recently discovered that very old versions of PD.jl are still around in the package manager:

pkg> add PowerDyn...
PowerDynOperationPoint PowerDynBase            PowerDynamics           PowerDynSolve

@SabineAuer Is this intended?

Design of faults.

This issue is to discuss the structure of the implementation of faults. I started looking into building a generic design using callbacks to swap between normal and fault dynamics. However, this is more tricky than maybe expected. Reinitializing the integrator after switching the functions somehow triggers the callback again.

I have pushed this experiment to this branch:
https://github.com/JuliaEnergy/PowerDynamics.jl/tree/AlternativeDesign-faults

A short discussion on this design is here:
https://discourse.julialang.org/t/changing-the-righthand-side-in-a-callback/27462/6

I don't know where to best go from here. I don't think the design with a switch is good for us. Maybe someone can get the reinit based code to work. Or we resurrect the composite solution that Tim built (and maybe try to upstream it?)

compat issues reloaded

Apparently the issue #89 is back since we upgraded to 0.3.1

if I install ND 0.3.1 first, and then PD I get version 2.2
if I install PD 2.3 first, and then ND I get version 0.1

add bus names to node list

instead of only having a line list and simply taking the entry as bus number we want to add bus names to the node list. This could look like

nodelist =[]
nodelist .add(“BusName”, SwingEq(...))
linelist=[]
linelist.add(“BusName1”,”BusName”,StaticLine(..))

This implies the json-format parser would need to be changed. We do not make the bus name mandatory but suggest to autgenerate the bus names if not given to not break the running json-import functionality

Error when trying to remove a node (reducing number of states)

Hi,

I get an bounds error if I try to remove two lines with a bus inbetween by a discrete callback (e.g. after a fault, however the fault is not explicitly respected here)):

BoundsError: attempt to access 4-element Array{Float64,1} at index [5]

The reason why I am doing this is that I try to simulate a fault that happens in the middle of a line (therefore the 'virtual' bus "busv") with a subsequent disconnection of both lines (which would be one line in reality).

Inside the callback the two states of the removed bus are deleted with deleteat!(integrator,3:4), here by hand. Afterwards, the integrator is updated with the ODEFunction of the new 'post-fault' grid and suitable initial conditions. In my case it is sufficient that the post-fault powergrid is build by hand.

I also tried to use reinit!, deleteat_non_user_cache!, resize! and resize_non_user_cache! but they did not help and the same error occurs. I used them because some things from the old grid still seem to exist in integrator.cache, but maybe I misunderstood how to use these functions.

I know that I could avoid this by terminating the integrator and starting a new simulation with the last states, but its not really convinient to interact with both solutions afterwards.

Do you know how to handle this, if the size (states) of a PG changes?

Here is my MWE:

using DifferentialEquations
using PowerDynamics
using OrderedCollections: OrderedDict
using Plots

Ubase = 380e3
Sbase  = 100e6
Zbase = Ubase^2/Sbase
Z_EHV_Line = (9.6 + 1im*64)/Zbase
B_half     = 1im*1498.54*1e-6 / 2.0 *Zbase

buses=OrderedDict(
    "bus1" => SlackAlgebraic(U=1.0),
    "busv" => VoltageDependentLoad(P=0.0, Q=0.0, U=1.0, A=0., B=0.,Y_n = complex(0.0)),
    "bus2" => VoltageDependentLoad(P=-1.0, Q=-0.5, U=1.0, A=0., B=0.,Y_n = complex(0.0)))

branches=OrderedDict(
    "Line_1-2"=> PiModelLine(from= "bus1", to = "bus2",y=1.0/Z_EHV_Line, y_shunt_km=B_half, y_shunt_mk=B_half),
    "Line_1-v"=> PiModelLine(from= "bus1", to = "busv",y=1.0/(Z_EHV_Line/2.0), y_shunt_km=B_half, y_shunt_mk=0.0),
    "Line_v-2"=> PiModelLine(from= "busv", to = "bus2",y=1.0/(Z_EHV_Line/2.0), y_shunt_km=0.0, y_shunt_mk=B_half))

buses_postfault =OrderedDict(
    "bus1" => SlackAlgebraic(U=1.0),
    "bus2" => VoltageDependentLoad(P=-1.0, Q=-0.5, U=1.0, A=0., B=0.,Y_n = complex(0.0)))

branches_postfault=OrderedDict(
    "Line_1-2"=> PiModelLine(from= "bus1", to = "bus2",y=1.0/Z_EHV_Line, y_shunt_km=B_half, y_shunt_mk=B_half))

pg = PowerGrid(buses, branches)
pg_postfault = PowerGrid(buses_postfault, branches_postfault)

function switch_off(integrator)
    deleteat!(integrator,3:4)
    #deleteat_non_user_cache!(integrator,3:4)     #tried, but same error
    #resize!(integrator,4)                           #tried, but same error
    #resize_non_user_cache!(integrator,4)            #tried, but same error
    integrator.f = rhs(pg_postfault)
    ic_temp = find_operationpoint(pg_postfault)
    integrator.u = ic_temp.vec
   # reinit!(integrator,ic_temp.vec)  #tried also, but same error
end

tfault =1.0
cb = DiscreteCallback(((u,t,integrator) -> t in tfault), switch_off)
ic = find_operationpoint(pg)
prob = ODEProblem(rhs(pg),ic.vec,(0.0,5.0))

pgsol = solve(prob,Rodas4(),callback =cb,tstops=[tfault])

Power Perturbation Simulation

Any plans to include a function to perturb power of generator similar to speed (sim-
ulate(Perturbation(bus_number, :omega, Inc(perturbation)))?

Compatibility with NetworkDynamics 0.3.0

We released a new version of NetworkDynamics. Computations can be executed on parallel architectures if network_dynamics is called with the keyword parallel=true. It should be fully compatible with the current version of PowerDynamics. However I noticed an issue with Pkg and the current versioning:

  • upgrading to NetworkDynamics 0.3.0 leads to a downgrade of PowerDynamics 2.3.0 -> 2.2.0.

  • when installing PowerDynamics 2.3.0 in a new environment and then adding NetworkDynamics, iit gets added in version 0.1.0

Docs: Links to source code not working

I was just reading the docs for SlackAlgebraic. The source code link redirects to JuliaBase. This happens for other node types aswell (SwingEq) though not for all of them.

PS I just realized that I already mentioned it in the "Update docs" issue, but maybe it is more clear to have a seperate issueon this, since at last PD.jl workshop another attendee had the same problem.

LineShortCircuit + rename LineFault ?

The name LineFault might create the impression that it creates a short circuit. What it does instead is actually removing a line. Maybe we can find a clearer name?

LineFailure maybe?

Anyway, I see actually a larger implementation task when it comes to short circuits on lines.

Up to now NodeShortCircuit actually is a line short circuit at the end of a line.
In practice, however, it can appear at any position.
You can think of it as splitting the line in two with an algebraic shunt bus inbetween.
For a PiModelLine, it is straight-forward to calculate the equivalent resistances and construct a faulted pi model.
But what about other types like transformers (different parameters, not obvious how to change the pi model) or dynamic lines? By now, I have to convert my model manually to include only PiModelLines, this is quite hacky though.

I have the impression that an alternative could be to wrap the vertex/edge function of a faulted component and add the fault current on top, working for all types that return a current and have voltages as input.

Though the cleaner way might be to really "split" the line and add an algebraic constraint for the shunt in the middle.

@JuliaEnergy/powerdynamics What are your thoughts on this?

Have releases automerged on Julia registry

Right now releases need to be manually merged into the main Julia registry. The new system allows automerging if all dependencies are specified with an entry in the compat section in Project.toml.

Adding classic power flow algorithm for initialisation

Hi,

I talked to @luap-pik some time ago to post my request here. I want to make a suggestion and a first draft for a classic power flow calculation algorithm which includes the definition of PQ, PV and slack (only one) nodes. The motivation is that the finding of the initial operating point is sometimes tricky for some types of grids, especially with reactive power limits or without any SlackAlgebraic node. The result of the power flow is given back as an initial guess for the dynamic simulation where the derivatives are zero. Maybe this can be combined with find_initial_operatingpiont.
To check the power flow by the user, also the voltage magnitude and the voltage angles in degree are returned (but this could also be ommited).

The slack node can either specified by the user or the Slackalgebraic node (if present) is taken. If a SlackAlgebraic node is present then the "user node" is ignored. If no SlackAlgebraic node is available and no node has been specified, then the first bus is taken as slack. Furthermore, it is relatively easy to extend the power flow to new node types, as can be seen by the first functions of the code.

The power flow is capable of changing PV nodes into PQ nodes if the reactive power limits of each node are reached. In addition, some extras like Iwamoto multipliers and the pre-calculation of voltage angles are included to increase the robustness of the power flow. Also the voltage dependency of the loads are included.

Update (16.03.21):
The vector of SI voltages as well as the base voltage are not longer necessary for the power flow calculation. Now the admittances of each branch have to be related directly to the desired voltage level (see branch 4). This has the advantage, that the admittances can also be used directly for the dynamic simulation. In terms of consistent modelling, I think this is more stringent. I also updated the power flow code below.

However, until now there are some small drawbacks. Up to now only the reference voltages of the SlackAlgebraic and the PVAlgebraic nodes are taken from the nodes for the load flow. For all other PV nodes 1.0 p.u. is the reference value (if it is a PV node). Of course you can specify voltages for FourthOrderEq but then you will need another routine that updates the field voltage of the machine, as it depends on the load flow. But maybe this is another topic.

In addition, the code could still be optimised in some places to be faster, but I am still relatively new with Julia.

Here is a small example and the call of the power flow could look like this:

Ubase = 380e3

buses=OrderedDict(
        "bus1"=> SlackAlgebraic(U=0.98),
        "bus2"=> VoltageDependentLoad(P=-0.3, Q=0.3, U=1.0, A=0.0, B=0.0),
        "bus3"=> FourthOrderEq(T_d_dash=6.1, D=2, X_d=1.05, X_q=0.98, Ω=50, X_d_dash=0.185, T_q_dash=0.4, X_q_dash=0.36, P=0.5, H=6.54, E_f= 1),
        "bus4"=> VoltageDependentLoad(P=-0.5, Q=-0.5, U=1.0, A=0.5, B=0.3),
        "bus5"=> PVAlgebraic(P=-0.0, V = 1.01))

branches=OrderedDict(
        "branch1"=> PiModelLine(from= "bus1", to = "bus2",y=1.0/(0.05+1im*0.15), y_shunt_km=0., y_shunt_mk=0.),
        "branch2"=> PiModelLine(from= "bus2", to = "bus3",y=1.0/(0.05+1im*0.15), y_shunt_km=0., y_shunt_mk=0.),
        "branch3"=> Transformer(from= "bus3", to = "bus4",y=1.0/(0.05+1im*0.15), t_ratio = 1.0/0.93),
        "branch4"=> PiModelLine(from= "bus4", to = "bus5",y=1.0/((0.05+1im*0.15)*(Ubase/110e3)^2), y_shunt_km=0., y_shunt_mk=0.))
#note that branch 4 has to be transformed to higher voltage level!
powergrid = PowerGrid(buses, branches)
#Unodes = [380e3,380e3,380e3,110e3,110e3]
#Ubase  = 380e3
Qmax   = [Inf, Inf, 0.5, Inf, Inf]
Qmin   = -Qmax
U,δ1,ic = PowerFlowClassic(powergrid,iwamoto = true, Qmax = Qmax, Qmin = Qmin, Qlimit_iter_check = 2)

And here is the code of the power flow:

using PowerDynamics
using PowerDynamics: guess
import PowerDynamics: PiModel
using LightGraphs #incidence_matrix
using Roots #for Iwamoto multiplier
using LinearAlgebra #for norm

#Pi models for nodal admittance matrice
PiModel(L::PiModelLine) = PiModel(L.y,L.y_shunt_km,L.y_shunt_mk,1,1)
PiModel(T::Transformer) = PiModel(T.y,0,0,T.t_ratio,1)
PiModel(S::StaticLine)  = PiModel(S.Y,0,0,1,1)
PiModel(R::RLLine)      = PiModel(1/(R.R+1im*R.ω0*R.L),0,0,1,1)

# NodeTypes: 0 = Slack, 1 = PV, 2 = PQ
NodeType(S::SlackAlgebraic) = 0
NodeType(F::FourthOrderEq)  = 1
NodeType(F::FourthOrderEqExciterIEEEDC1A)  = 1
NodeType(F::FourthOrderEqGovernorExciterAVR)  = 1
NodeType(F::FourthOrderEqGovernorIEEEG1)  = 1
NodeType(S::SwingEq)  = 1
NodeType(L::PVAlgebraic) = 1
NodeType(V::VSIMinimal) = 2
NodeType(V::VSIVoltagePT1) = 2
NodeType(L::PQAlgebraic) = 2
NodeType(L::VoltageDependentLoad) = 2
NodeType(L::ExponentialRecoveryLoad)  = 2
NodeType(L::CSIMinimal)  = 2

#note: only loads are treated with voltage depency and are called every iteration
PowerNodeLoad(S::SlackAlgebraic,U) = 0. #treated as generation
PowerNodeLoad(F::FourthOrderEq,U) = 0. #treated as generation
PowerNodeLoad(F::FourthOrderEqExciterIEEEDC1A,U)  = 0. #treated as generation
PowerNodeLoad(F::FourthOrderEqGovernorExciterAVR,U)  = 0. #treated as generation
PowerNodeLoad(F::FourthOrderEqGovernorIEEEG1,U)  = 0. #treated as generation
PowerNodeLoad(S::SwingEq,U)  = 0. #treated as generation
PowerNodeLoad(V::VSIMinimal,U) = complex(V.P,(abs(U)-V.V_r)/V.K_Q+V.Q)   #treated as load with changed sign to include voltage dependency
PowerNodeLoad(V::VSIVoltagePT1,U) = complex(V.P,(abs(U)-V.V_r)/V.K_Q+V.Q)   #treated as load with changed sign to include voltage dependency
PowerNodeLoad(L::PVAlgebraic,U) = L.P  #treated as load
PowerNodeLoad(L::PQAlgebraic,U) = complex(L.P,L.Q) #treated as load
PowerNodeLoad(L::VoltageDependentLoad,U) = complex(L.P, L.Q) * (L.A * abs(U)^2 + L.B * abs(U) + 1 - L.A - L.B)
PowerNodeLoad(L::ExponentialRecoveryLoad,U)  = (L.P0*((abs(U)/L.V0)^L.Nps) + 1im*L.Q0*((abs(U)/L.V0)^L.Nqs))
PowerNodeLoad(L::CSIMinimal,U)  = -U*conj(L.I_r)

#generation is voltage independent, otherwise it has to be called every iteration
PowerNodeGeneration(S::SlackAlgebraic) = 0.
PowerNodeGeneration(F::FourthOrderEq) = F.P
PowerNodeGeneration(F::FourthOrderEqExciterIEEEDC1A)  = F.P
PowerNodeGeneration(F::FourthOrderEqGovernorExciterAVR)  = F.P
PowerNodeGeneration(F::FourthOrderEqGovernorIEEEG1)  =  F.P
PowerNodeGeneration(S::SwingEq)  = S.P
PowerNodeGeneration(V::VSIMinimal) = 0. #treated as load with changed sign to include voltage dependency
PowerNodeGeneration(V::VSIVoltagePT1) = 0. #treated as load with changed sign to include voltage dependency
PowerNodeGeneration(L::PVAlgebraic) = 0. #treated as load
PowerNodeGeneration(L::PQAlgebraic) = 0. #treated as load
PowerNodeGeneration(L::VoltageDependentLoad) = 0. #treated as load
PowerNodeGeneration(L::ExponentialRecoveryLoad)  = 0. #treated as load
PowerNodeGeneration(L::CSIMinimal)  = 0. #treated as load


function PowerFlowClassic(pg::PowerGrid; ind_sl::Int64 = 0,max_tol::Float64 = 1e-7,iter_max::Int64  = 30,iwamoto::Bool =false, Qmax = -1, Qmin = -1, Qlimit_iter_check::Int64 = 3)
    number_nodes = length(pg.nodes); #convenience
    nodetypes = NodeType.(values(pg.nodes))
    if !isempty(findall(x-> x==0, nodetypes)) #if there is no SlackAlgebraic
        ind_sl = findall(x-> x==0, nodetypes)[1] #set passed value
        @info "Reference node is bus no. $ind_sl"
    end
    ind_PV_or = findall(x-> x==1, nodetypes)
    ind_PQ_or = findall(x-> x==2, nodetypes)

    ind_PQ = deepcopy(ind_PQ_or) #copy is needed for checking Qlimits
    ind_PV = deepcopy(ind_PV_or) #copy is needed for checking Qlimits

    #calculate Ykk
    Ykk = NodalAdmittanceMatrice(pg);

    U = ones(number_nodes,1);
    if SlackAlgebraic  collect(values(pg.nodes)) .|> typeof
        U[ind_sl] = collect(values(pg.nodes))[ind_sl].U
    end
    if PVAlgebraic  collect(values(pg.nodes)) .|> typeof
        pv = findall(collect(values(pg.nodes).|> typeof).== PVAlgebraic)
        for i in pv
            U[i] = collect(values(pg.nodes))[i].V
        end
    end
    δ = CalcδStartValues(pg,Ykk,ind_sl);

    Ykk_abs = abs.(Ykk);
    θ   = angle.(Ykk);
    Pn  = similar(U);
    Qn  = similar(U);

    S_node_gen = Array{Complex{Float64},2}(undef,number_nodes,1)
    S_node_load = Array{Complex{Float64},2}(undef,number_nodes,1)
    #PowerNodeGeneration is called only once, since it should only change,
    #when power limits are reached or voltage dependency should be included.
    #However, both are not included yet.
    for (ind,val) in enumerate(values(pg.nodes)) S_node_gen[ind] = PowerNodeGeneration(val) end
    #start of the iterationen
    for iter in 1:iter_max+1
        for i in 1:number_nodes #calculate node powers with current voltage
            Pn[i] = sum(U[i].*U.*Ykk_abs[:,i].*cos.(δ[i].-δ.-θ[:,i]))
            Qn[i] = sum(U[i].*U.*Ykk_abs[:,i].*sin.(δ[i].-δ.-θ[:,i]))
        end
        #update load powers
        for (ind,val) in enumerate(values(pg.nodes)) S_node_load[ind] = PowerNodeLoad(val,U[ind]*exp(1im*δ[ind])) end

        S_node = S_node_gen + S_node_load #total power at a node

        ΔP = real(S_node) - Pn
        ΔQ = imag(S_node) - Qn

        ΔP = ΔP[1:end .!= ind_sl, :]; #delete slack
        ΔQ = ΔQ[setdiff(1:end, [ind_sl; ind_PV]), :]; #delete slack and PV nodes

        error_ = norm([ΔP;ΔQ],Inf)
        if error_ < max_tol
            @info "Power flow converged in $iter iterations"
            ic_guess = guess.(values(pg.nodes),U.*exp.(1im.*δ))
            return U,δ*180/pi,vcat(ic_guess...) # power flow converged
        elseif iter == iter_max
             @warn "Power flow reached max. iteration ($iter) and has not converged."
             ic_guess = guess.(values(pg.nodes),U.*exp.(1im.*δ))
             return U,δ*180/pi,vcat(ic_guess...) #max iteration reached
        end

        #get load flow Jacobian
        J = CalculatePolarLoadFlowJacobian(U,δ,Ykk)
        del_sl = [ind_sl;ind_sl+number_nodes] #position of slack in J
        del_PV = collect(ind_PV.+number_nodes) #position of PV nodes in J

        #delete those positions
        J = J[setdiff(1:end, [del_sl;del_PV]), setdiff(1:end, [del_sl;del_PV])];

        res = J \ [ΔP; ΔQ];

        #With Iwamoto mulipliers the load flow is more robust, but slower
        if iwamoto
            iwa = CalcIwamotoMultiplier(J,res,ΔP,ΔQ,Ykk,ind_sl,ind_PV,ind_PQ,number_nodes);
            res *= iwa
        end

        #split result in angle and magnitude
        Δδ  = res[1:length(ΔP)]
        ΔU  = res[length(ΔP)+1:end]

        #Update angle and voltage magnitude, where the latter is the square
        δ[1:end .!= ind_sl, :] += Δδ
        U[setdiff(1:end, [ind_sl; ind_PV]), :] = U[setdiff(1:end, [ind_sl; ind_PV]), :] + ΔU.*U[setdiff(1:end, [ind_sl; ind_PV]), :]

        #Check if reactive power limit is reached; change PV to PQ node
        if Qmax != -1 && Qmin != -1 #Qmax and Qmin are normally vectors with the limits
            #In future, a routine is needed which will get the information about
            #the reactive power limits from each node directly
            if mod(iter,Qlimit_iter_check) == 0 && !isempty(ind_PV_or) #dont change every iteration and check if PV nodes exist
                for i in ind_PV_or # Calc current reactive power at each node
                    Qn[i] = sum(U[i].*U.*Ykk_abs[:,i].*sin.(δ[i].-δ.-θ[:,i]))
                    if Qn[i] >= Qmax[i]
                        index = findall(x-> x==i ,ind_PV) #find the PV node index
                        if ~isempty(index)
                            ind_PV = ind_PV[1:end .!= index, 1]; #delete it from the PV node index list
                        end
                        if isempty(findall(x-> x==i ,ind_PQ))
                            append!(ind_PQ,i)  #append it to the PQ list, but only once
                        end
                        S_node_gen[i] = real(S_node_gen[i]) + 1im*Qmax[i]
                    elseif Qn[i] <= Qmin[i] #same here of lower limits
                        index = findall(x-> x==i ,ind_PV)
                        if ~isempty(index)
                            ind_PV = ind_PV[1:end .!= index, 1];
                        end
                        if isempty(findall(x-> x==i ,ind_PQ))
                            append!(ind_PQ,i)  #append it to the PQ list, but only once
                        end
                        S_node_gen[i] = real(S_node_gen[i]) + 1im*Qmin[i]
                    elseif isempty(findall(x-> x==i ,ind_PV)) #if inside limits and not in list, set as PV node again
                        index = findall(x-> x==i ,ind_PQ)
                        if ~isempty(index)
                            ind_PQ = ind_PQ[1:end .!= index, 1]; #delete PQ node
                        end
                        append!(ind_PV,i)#append it to the PV list
                    end
                end
            end
        end
    end
end

function NodalAdmittanceMatrice(pg::PowerGrid)
    Fourpoles = PiModel.(values(pg.lines));
    #changing sign convention from PowerDynamics
    for i in 1:length(Fourpoles)
        Fourpoles[i][1,:] *= -1
    end
    B = Array{Complex{Float64},2}(undef,0,0);
    for i in Fourpoles
        B = vcat(hcat(B,zeros(size(B)[1],2)),hcat(zeros(2,size(B)[1]),i))
    end
    #create incidence matrix, but different to LightGraphs
    inci = incidence_matrix(pg.graph, oriented = false);
    inci_new = zeros(size(inci)[1],2*size(inci)[2]);
    for i in 1:size(inci)[2]
        ind = findall(x->x==1,inci[:,i])
        inci_new[ind[1],2*i-1] = 1
        inci_new[ind[2],2*i] = 1
    end
    Ykk = inci_new*B*inci_new'
end

function CalcδStartValues(pg,Ykk,ind_sl)
    # Calculates voltage angles based on an 'idle grid' (leerlaufendes netz)
    # Increases power flow robustness for grids with high line impedances
    # more infos (on p. 338):
    # Bernd R. Oswald,
    # Berechnung von Drehstromnetzen, 3. Auflage, Springer Verlag, 2017
    u_sl = 1.
    try #if slack is SlackAlgebraic
        u_sl = collect(values(pg.nodes))[ind_sl].U
    catch #otherwise take slack voltage as 1, could be improved in the future
        u_sl = 1.
    end
    Ykkr  = copy(Ykk)
    Yk_sl = Ykk[:,ind_sl]
    Ykkr[:,ind_sl] .= 0.
    Ykkr[ind_sl,:] .= 0.
    Ykkr[ind_sl,ind_sl] = -Ykk[ind_sl,ind_sl]
    uk   = -inv(Ykkr)*Yk_sl*u_sl
    return angle.(uk)
end

function CalculatePolarLoadFlowJacobian(U,δ,Ykk)
    number_nodes = length(U)
    Ykk_abs = abs.(Ykk)
    θ = angle.(Ykk)
    dPdδ = zeros(number_nodes,number_nodes)
    dPdU = zeros(number_nodes,number_nodes)
    dQdδ = zeros(number_nodes,number_nodes)
    dQdU = zeros(number_nodes,number_nodes)
    for i in 1:number_nodes
        ind_= [collect(1:i-1); collect(i+1:number_nodes)]; #index list without current index

        dPdδ[i,:]  = U[i].*U.*Ykk_abs[:,i].*sin.(δ[i].-δ.-θ[:,i]);
        dPdδ[i,i]  = -1*sum(dPdδ[i,ind_]);

        dPdU[i,:]  = -U[i].*U.*Ykk_abs[:,i].*cos.(δ[i].-δ.-θ[:,i]);
        dPdU[i,i]  = -sum(dPdU[i,ind_]);

        dQdδ[i,:]  = U[i].*Ykk_abs[:,i].*cos.(δ[i].-δ.-θ[:,i]);
        dQdδ[i,i]  = 2.0*U[i].*Ykk_abs[i,i].*cos.(θ[i,i]).+sum(U[ind_].*Ykk_abs[ind_,i].*cos.(δ[i].-δ[ind_].-θ[ind_,i]));

        dQdU[i,:]  = U[i].*Ykk_abs[:,i].*sin.(δ[i].-δ.-θ[:,i]);
        dQdU[i,i]  = (2.0*(U[i].^2).*Ykk_abs[i,i].*sin(-θ[i,i]).-dPdδ[i,i])./U[i];
    end
    return J = [dPdδ dQdδ; dPdU dQdU]
end

function CalcIwamotoMultiplier(J,res,ΔP,ΔQ,Ykk,ind_sl,ind_PV,ind_PQ,number_nodes)
    # For more information:
    #  S. Iwamoto and Y. Tamura,
    # "A Load Flow Calculation Method for Ill-Conditioned Power Systems,"
    # in IEEE Transactions on Power Apparatus and Systems, vol. PAS-100, no. 4,
    # pp. 1736-1743, April 1981, doi: 10.1109/TPAS.1981.316511.
    ΔU = zeros(number_nodes,1)
    ΔU[setdiff(1:end, [ind_sl; ind_PV]), :] = res[length(ΔP)+1:end]

    Δδ = zeros(number_nodes,1)
    Δδ[setdiff(1:end, [ind_sl]), :] = res[1:length(ΔP)]

    ΔUc = ΔU.*exp.(1im*Δδ)
    ΔS  = ΔUc.*(conj.(Ykk)*conj.(ΔUc))
    ΔS  = ΔS[1:end .!= ind_sl, :];

    ΔQ_tmp = zeros(number_nodes,1)
    ΔQ_tmp[intersect(1:end, ind_PQ), :] = ΔQ
    ΔQ_tmp  = ΔQ_tmp[1:end .!= ind_sl, :];

    a = [ΔP;ΔQ_tmp]
    b = -a;
    c = -[real(ΔS);imag(ΔS)]

    g0 = sum(a.*b);
    g1 = sum(b.*b+ 2.0*a.*c);
    g2 = sum(3.0*b.*c);
    g3 = sum(2.0*c.*c);

    f(x) = g3*x^3+g2*x^3+g1*x+g0
    return  find_zero(f,1.0)
end

Ad-hoc definition of node types

Problem:
I want to use a custom node type for an analysis and do this using the @DynamicNode macro.
However, several internal methods are created for the new type and work in my script but are not available to other functions in PowerDynamics (see code example below).

Workaround:
I need to dev PowerDynamics.jl locally and add the new node type to the source s.t. it's available via using PowerDynamics.

Sidenote:
I also needed manual imports to make the macro run. This might be related.

import Base: @__doc__ #-> LoadError: UndefVarError: @__doc__ not defined
import PowerDynamics: AbstractNode #-> UndefVarError: AbstractNode not defined

Example:

using PowerDynamics
import Base: @__doc__ #-> LoadError: UndefVarError: @__doc__ not defined
import PowerDynamics: AbstractNode #-> UndefVarError: AbstractNode not defined


@DynamicNode MySwingEq(P, H, D, Ω) begin
PowerDynamics.MassMatrix(;m_u = true, m_int = [1, 0])
end 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

showdefinition(stdout, MySwingEq) |> println

swing_node = MySwingEq(P=1., H=1., D=0.1, Ω=50.)
slack = SlackAlgebraic(U=1.)

nodes = [swing_node, slack]

lines = [StaticLine(from=1, to=2, Y=1. + 5. * im)]

pg = PowerGrid(nodes, lines)

# The methods created by the macro are working here...

dimension(swing_node)
symbolsof(swing_node)

# The following two lines both fail with:
#   MethodError: no method matching dimension(::MySwingEq)
# This leads to subsequent errors e.g. in find_operationpoint or find_valid_initial_condition.

systemsize(pg)

Three Phase Fault on a Bus

Similar to the (simulate(Perturbation(2, :omega, Inc(0.2))) function, it would be helpful to have a function to simulate a three phase fault on a bus.

Coordinate system transformations

When thinking about various design questions for the operating point and the three phase model it occurred to me that it might be valuable to allow for a change of the co-rotating frame. This would require every node to explicitly include a variable/parameter \omega_r, and express the equations in such a way as to be universally valid (for example by multiplying e^{i \omega_r t} at appropriate points).

The advantage of this would be that we could vary the \omega_r in the operating point search, and that we would have a clear definition for our dq0 system in terms of our rated frequency.

I'm not sure how much work that would be or how worthwhile it would really be to really do this though...

TagBot trigger issue

This issue is used to trigger TagBot; feel free to unsubscribe.

If you haven't already, you should update your TagBot.yml to include issue comment triggers.
Please see this post on Discourse for instructions and more details.

If you'd like for me to do this for you, comment TagBot fix on this issue.
I'll open a PR within a few hours, please be patient!

examples folder in PowerDynamics.jl

I really like having the PowerDynamicsExamples repo in order to connect this to binderhub but I think (similar to PyPSA) an examples folder directly in the PowerDynamics repo with only one or two files users can try out would be helpful. For example, adding a new feature it is possible to try it out with an example script right ahead.

Possibility to Import/Export a powergrid

It would be good to have an internal file format, like Json for importing and exporting
a given grid. The existing CSVParser only supports importing from CSV but not exporting back. It also has several limitations and hardcoded assumptions baked into and does not support all node & line types.

putting module in develop mode

Hi,

Thanks for this great package, it's a great contribution to the power systems research community! I'm trying to figure out the behavior and design of the module, and would like to put it in dev mode through the built-in package manager, i.e. ] develop PowerDynamics.

I get the following error, on mac (julia 1.5):

(@v1.5) pkg> develop PowerDynamics
    Cloning git-repo `https://github.com/JuliaEnergy/PowerDynamics.jl.git`
ERROR: failed to clone from https://github.com/JuliaEnergy/PowerDynamics.jl.git, error: GitError(Code:ERROR, Class:OS, could not remove directory '/Users/get050/.julia/dev/PowerDynamics/.git/refs/remotes/origin/compathelper/new_version/2020-08-01-00-06-19-852-2271493462': parent is not directory: Not a directory)

and an almost identical error on Windows as well.

I can put other modules in dev mode successfully, so it seems to have something to do with the git config of this repo. The first google searches don't really lead to a solution (deleting .julia and/or its subfolders). Do you have any idea what could be causing this?

update to Documenter.jl v0.20

Documenter.jl has been updated to v0.20 with breaking changes. Implement these changes for PowerDynamics.jl and subpackages.

WIP: operation point search

(idea collection)

We need to develop a better operation point search that also needs to be more robust w.r.t
the initial guess. It should also have good performance for large systems.

Probably, it should be a multi-step approach consisting of

  1. good heuristics for initial values
  2. automatic load flow solution
  3. use 1 and 2 to get a compatible initial condition for the whole system, find a stable fixpoint

Research other options to solve power flow:

  • PowerModels.jl (based on JuMP)
  • interface to Panda Power
  • do we need HELM?

`import` statements over `using`

Hi @FHell, @SabineAuer, @luap-pik, @antonplietzsch,

Lucas, (@Lima-Lima), mentioned here, that it is actually a good idea to use only import statements instead of using inside of PowerDynamics packages to keep the namespace clean.

Do you have any opinions on that?

I personally think, that it would be a really good idea for the following reasons:

  • clean namespace, you always know whats loaded
  • stability againsnt future changes: If other packages start to export objects with the same name that we use, PowerDynamics may break with using
  • @WouterVer had some problems when getting into PowerDynamics as to know where what comes from. This should be easier when using import.

Cheers,
Tim

PS: It's a bit embarrassing that I didn't think of that myself (:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.