# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pandas as pd
import timeit
from dataclasses import dataclass
from typing import Tuple, Union
from scipy import sparse as sp
from scipy.sparse import csc_matrix as csc
from scipy.sparse import lil_matrix
from VeraGridEngine import ShuntControlMode
from VeraGridEngine.Utils.Sparse.csc import diags
from VeraGridEngine.Compilers.circuit_to_data import NumericalCircuit
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.common_functions import (compute_power, polar_to_rect)
from VeraGridEngine.Simulations.OPF.opf_options import OptimalPowerFlowOptions
from VeraGridEngine.enumerations import AcOpfMode, BusMode, TapPhaseControl, TapModuleControl, ShuntControlMode
from VeraGridEngine.basic_structures import Vec, CxVec, IntVec, Logger, csr_matrix, csc_matrix
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.common_functions import get_Sf, get_St
from VeraGridEngine.Simulations.OPF.NumericalMethods.newton_raphson_ips_fx import IpsSolution
[docs]
@dataclass(slots=True)
class NonlinearOPFResults:
"""
Numerical non linear OPF results
"""
Va: Vec = None
Vm: Vec = None
S: CxVec = None
Sf: CxVec = None
St: CxVec = None
loading: Vec = None
Pg: Vec = None
Qg: Vec = None
Qsh: Vec = None
Pcost: Vec = None
tap_module: Vec = None
tap_phase: Vec = None
hvdc_Pf: Vec = None
hvdc_loading: Vec = None
# AC/DC + VSC extension
vsc_Pt: Vec = None
vsc_Qt: Vec = None
vsc_Pf: Vec = None
vsc_It: Vec = None
vsc_loading: Vec = None
lam_p: Vec = None
lam_q: Vec = None
sl_sf: Vec = None
sl_st: Vec = None
sl_vmax: Vec = None
sl_vmin: Vec = None
nodal_capacity: Vec = None
error: float = None
converged: bool = None
iterations: int = None
voltage: CxVec = None
[docs]
def initialize(self, nbus: int, nbr: int, nil: int, nsh: int, ng: int, nhvdc: int, ncap: int,
nvsc: int = 0):
"""
Initialize the arrays
:param nbus: number of buses
:param nbr: number of branches
:param nsh: number of controllable shunt elements
:param ng: number of generators
:param nhvdc: number of HVDCs
:param ncap: number of nodal capacity nodes
:param nvsc: number of VSCs
"""
self.Va: Vec = np.zeros(nbus)
self.Vm: Vec = np.zeros(nbus)
self.S: CxVec = np.zeros(nbus, dtype=complex)
self.Sf: CxVec = np.zeros(nbr, dtype=complex)
self.St: CxVec = np.zeros(nbr, dtype=complex)
self.loading: Vec = np.zeros(nbr)
self.Pg: Vec = np.zeros(ng)
self.Qg: Vec = np.zeros(ng)
self.Qsh: Vec = np.zeros(nsh)
self.Pcost: Vec = np.zeros(ng)
self.tap_module: Vec = np.zeros(nbr)
self.tap_phase: Vec = np.zeros(nbr)
self.hvdc_Pf: Vec = np.zeros(nhvdc)
self.hvdc_loading: Vec = np.zeros(nhvdc)
self.vsc_Pt: Vec = np.zeros(nvsc)
self.vsc_Qt: Vec = np.zeros(nvsc)
self.vsc_Pf: Vec = np.zeros(nvsc)
self.vsc_It: Vec = np.zeros(nvsc)
self.vsc_loading: Vec = np.zeros(nvsc)
self.lam_p: Vec = np.zeros(nbus)
self.lam_q: Vec = np.zeros(nbus)
self.sl_sf: Vec = np.zeros(nil)
self.sl_st: Vec = np.zeros(nil)
self.sl_vmax: Vec = np.zeros(nbus)
self.sl_vmin: Vec = np.zeros(nbus)
self.nodal_capacity: Vec = np.zeros(ncap)
self.error: float = 0.0
self.converged: bool = False
self.iterations: int = 0
self.voltage: CxVec = np.zeros(nbus, dtype=complex)
[docs]
def merge(self,
other: "NonlinearOPFResults",
bus_idx: IntVec,
br_idx: IntVec,
il_idx: IntVec,
gen_idx: IntVec,
hvdc_idx: IntVec,
ncap_idx: IntVec,
contshunt_idx: IntVec,
acopf_mode,
vsc_idx: IntVec = None):
"""
:param other:
:param bus_idx:
:param br_idx:
:param il_idx:
:param gen_idx:
:param hvdc_idx:
:param ncap_idx:
:param contshunt_idx:
:param acopf_mode:
:return:
"""
self.Va[bus_idx] = other.Va
self.Vm[bus_idx] = other.Vm
self.voltage[bus_idx] = other.Vm * np.exp(1j * other.Va)
self.S[bus_idx] = other.S
self.Sf[br_idx] = other.Sf
self.St[br_idx] = other.St
self.loading[br_idx] = other.loading
self.Pg[gen_idx] = other.Pg
self.Qg[gen_idx] = other.Qg
self.Qsh[contshunt_idx] = other.Qsh
self.Pcost[gen_idx] = other.Pcost
self.tap_module[br_idx] = other.tap_module
self.tap_phase[br_idx] = other.tap_phase
self.hvdc_Pf[hvdc_idx] = other.hvdc_Pf
self.hvdc_loading[hvdc_idx] = other.hvdc_loading
if vsc_idx is not None and len(vsc_idx):
self.vsc_Pt[vsc_idx] = other.vsc_Pt
self.vsc_Qt[vsc_idx] = other.vsc_Qt
self.vsc_Pf[vsc_idx] = other.vsc_Pf
self.vsc_It[vsc_idx] = other.vsc_It
self.vsc_loading[vsc_idx] = other.vsc_loading
self.lam_p[bus_idx] = other.lam_p
self.lam_q[bus_idx] = other.lam_q
if ncap_idx is not None:
self.nodal_capacity[ncap_idx] = other.nodal_capacity
if acopf_mode == AcOpfMode.ACOPFslacks:
self.sl_sf[il_idx] = other.sl_sf
self.sl_st[il_idx] = other.sl_st
self.sl_vmax[bus_idx] = other.sl_vmax
self.sl_vmin[bus_idx] = other.sl_vmin
self.error: float = 0.0
self.converged: bool = False
self.iterations: int = 0
@property
def V(self) -> CxVec:
"""
Complex voltage
:return: CxVec
"""
return self.Vm * np.exp(1j * self.Va)
[docs]
class NonLinearOptimalPfProblem:
__slots__ = (
"options",
"nc",
"logger",
"optimize_nodal_capacity",
"capacity_nodes_idx",
"nodal_capacity_sign",
"results",
"Sbase",
"from_idx",
"to_idx",
"indices",
"slack",
"k_m",
"k_tau",
"k_mtau",
"slackgens",
"Sd",
"Pg_max",
"Pg_min",
"Qg_max",
"Qg_min",
"ngen",
"id_sh",
"sh_bus_idx",
"nsh",
"admittances",
"Qsh_max",
"Qsh_min",
"Vm_max",
"Vm_min",
"id_Vm_min0",
"id_Vm_max0",
"pf",
"tanmax",
"pv",
"pq",
"nbr",
"br_idx",
"br_mon_idx",
"gen_disp_idx",
"gen_disp_idx_sh",
"Cfmon",
"Cfmon_t",
"Ctmon",
"Ctmon_t",
"R",
"X",
"nbus",
"n_slack",
"ntapm",
"ntapt",
"npv",
"npq",
"n_br_mon",
"n_gen_disp",
"n_gen_disp_sh",
"ind_gens",
"gen_nondisp_idx",
"gen_bus_idx",
"Sg_undis",
"rates",
"rates2",
"Va_max",
"Va_min",
"Ybus_indptr",
"Ybus_cols",
"Ybus_indices",
"Ybus_diag_pos",
"Cdispgen",
"Cdispgen_t",
"Cdispgen_sh",
"Cdispgen_sh_t",
"Inom",
"c0",
"c1",
"c2",
"c0n",
"c1n",
"c2n",
"tapm_max",
"tapm_min",
"tapt_max",
"tapt_min",
"all_tap_m",
"all_tap_tau",
"hvdc_nondisp_idx",
"hvdc_disp_idx",
"f_nd_hvdc",
"t_nd_hvdc",
"Pf_nondisp",
"n_disp_hvdc",
"f_disp_hvdc",
"t_disp_hvdc",
"P_hvdc_max",
"nsl",
"c_s",
"c_v",
"nslcap",
"slcap0",
"neq",
"nineq",
"Pg",
"Qg",
"Vm",
"Va",
"tap_m",
"tap_tau",
"Pfdc",
# AC/DC + VSC extension
"dc_bus_idx",
"ac_bus_idx",
"n_dc_bus",
"vsc_idx",
"nvsc",
"F_vsc",
"T_vsc",
"vsc_alpha1",
"vsc_alpha2",
"vsc_alpha3",
"vsc_rate_pu",
"vsc_lim_idx",
"n_vsc_lim",
"n_vsc_vars",
"Pt_vsc",
"Qt_vsc",
"Pf_vsc",
"It_vsc",
"sl_vsc",
"c_vsc",
"n_sl_vsc",
"sl_sf",
"sl_st",
"sl_vmax",
"sl_vmin",
"slcap",
"x0",
"NV",
"V",
"Scalc",
"allSf",
"allSt",
"Sf",
"St",
"Sf2",
"St2",
)
def __init__(self,
nc: NumericalCircuit,
options: OptimalPowerFlowOptions,
logger: Logger,
pf_init: bool = True,
Sbus_pf: Union[CxVec, None] = None,
voltage_pf: Union[CxVec, None] = None,
optimize_nodal_capacity: bool = False,
nodal_capacity_sign: float = 1.0,
capacity_nodes_idx: Union[IntVec, None] = None,
):
self.options = options
self.nc = nc
self.logger = logger
self.optimize_nodal_capacity = optimize_nodal_capacity
self.capacity_nodes_idx = capacity_nodes_idx
self.nodal_capacity_sign = nodal_capacity_sign
self.results = NonlinearOPFResults()
# Parameters
self.Sbase = nc.Sbase
self.from_idx = nc.passive_branch_data.F
self.to_idx = nc.passive_branch_data.T
self.indices = nc.get_simulation_indices(Sbus=Sbus_pf)
self.slack = self.indices.vd
self.k_m = np.zeros(0, dtype=int)
self.k_tau = np.zeros(0, dtype=int)
self.k_mtau = np.zeros(0, dtype=int)
self.analyze_branch_controls()
# np.isin (not ==) so this works for >1 slack bus, which AC/DC grids need
# For the single-slack case this is identical to the previous behaviour
self.slackgens = np.flatnonzero(np.isin(self.nc.generator_data.get_bus_indices(), self.slack))
self.Sd = - nc.load_data.get_injections_per_bus() / self.Sbase
if optimize_nodal_capacity:
self.Pg_max = nc.generator_data.p / self.Sbase
self.Pg_min = nc.generator_data.p / self.Sbase
else:
self.Pg_max = nc.generator_data.pmax / self.Sbase
self.Pg_min = nc.generator_data.pmin / self.Sbase
self.Pg_max[self.slackgens] = nc.generator_data.pmax[self.slackgens] / self.Sbase
self.Pg_min[self.slackgens] = nc.generator_data.pmin[self.slackgens] / self.Sbase
self.Qg_max = nc.generator_data.qmax / self.Sbase
self.Qg_min = nc.generator_data.qmin / self.Sbase
self.ngen = len(self.Pg_max)
# Shunt elements are treated as generators with fixed P.
# As such, their limits are added in the generator limits array.
self.id_sh = np.where(nc.shunt_data.control_mode_int == ShuntControlMode.Continuous.idx())[0]
self.sh_bus_idx = nc.shunt_data.get_bus_indices()[self.id_sh]
self.nsh = len(self.id_sh)
# Since controllable shunts will be treated as generators, we deactivate them to avoid its computation in the
# Admittance matrix. Then, the admittance elements are stored.
# TODO: this modifies the original data, better to make a copy of Y for shunts
nc.shunt_data.Y[self.id_sh] = 0 + 0j
self.admittances = nc.get_admittance_matrices()
self.Qsh_max = nc.shunt_data.qmax[self.id_sh] / self.Sbase
self.Qsh_min = nc.shunt_data.qmin[self.id_sh] / self.Sbase
self.Pg_max = np.r_[self.Pg_max, np.zeros(self.nsh)]
self.Pg_min = np.r_[self.Pg_min, np.zeros(self.nsh)]
self.Qg_max = np.r_[self.Qg_max, self.Qsh_max]
self.Qg_min = np.r_[self.Qg_min, self.Qsh_min]
self.Vm_max = nc.bus_data.Vmax
self.Vm_min = nc.bus_data.Vmin
self.id_Vm_min0 = np.where(self.Vm_min == 0)[0]
if len(self.id_Vm_min0) != 0:
for i in self.id_Vm_min0:
self.logger.add_warning('Lower voltage limits are set to 0. Correcting to 0.9 p.u.',
device="Bus " + str(i))
self.Vm_min[self.id_Vm_min0] = 0.9
self.id_Vm_max0 = np.where(self.Vm_max < self.Vm_min)[0]
if len(self.id_Vm_max0) != 0:
for i in self.id_Vm_max0:
self.logger.add_warning('Upper voltage limits are set lower to the lower limit. Correcting to 1.1 p.u.',
device="Bus " + str(i))
self.Vm_max[self.id_Vm_max0] = 1.1
self.pf = nc.generator_data.get_pf()
self.tanmax = ((1 - self.pf ** 2) ** (1 / 2)) / (self.pf + 1e-15)
self.pv = np.flatnonzero(self.Vm_max == self.Vm_min)
self.pq = np.flatnonzero(self.Vm_max != self.Vm_min)
# AC/DC + VSC extension -------------------------------------------------------------
# DC buses: identified by the is_dc flag. Their voltage angle is meaningless, so the
# imaginary nodal-balance row is replaced by Va[dc] = 0 in update()
is_dc = np.asarray(nc.bus_data.is_dc, dtype=bool)
self.dc_bus_idx = np.flatnonzero(is_dc)
self.ac_bus_idx = np.flatnonzero(~is_dc)
self.n_dc_bus = len(self.dc_bus_idx)
# Monopolar VSCs: bus_from (F) is the DC+ bus, bus_to (T) is the AC bus. Controls are
# ignored; the converter is a free P/Q device bounded by its loss equation and AC
# current limit. Per-VSC state: Pt, Qt (AC side), Pf (DC side), It (current magnitude).
self.nvsc = int(nc.vsc_data.nelm)
self.vsc_idx = np.arange(self.nvsc, dtype=int)
self.F_vsc = np.asarray(nc.vsc_data.F, dtype=int)
self.T_vsc = np.asarray(nc.vsc_data.T, dtype=int)
self.vsc_alpha1 = np.asarray(nc.vsc_data.alpha1, dtype=float)
self.vsc_alpha2 = np.asarray(nc.vsc_data.alpha2, dtype=float)
self.vsc_alpha3 = np.asarray(nc.vsc_data.alpha3, dtype=float)
self.vsc_rate_pu = np.asarray(nc.vsc_data.rates, dtype=float) / self.Sbase
self.vsc_lim_idx = np.flatnonzero(self.vsc_rate_pu > 0.0)
self.n_vsc_lim = len(self.vsc_lim_idx)
# In ACOPFslacks mode the VSC current limit gets a soft slack
if options.acopf_mode == AcOpfMode.ACOPFslacks:
self.n_sl_vsc = self.n_vsc_lim
self.c_vsc = np.full(self.n_vsc_lim, 100.0)
else:
self.n_sl_vsc = 0
self.c_vsc = np.zeros(0)
# New variables appended at the end of x: Pt, Qt, Pf, It (4*nvsc) + sl_vsc
self.n_vsc_vars = 4 * self.nvsc + self.n_sl_vsc
# DC has no reactive power: set Q = 0 for dispatchable generators sitting on DC buses
if self.n_dc_bus and self.ngen:
gen_is_dc = is_dc[nc.generator_data.get_bus_indices()]
dc_gen = np.flatnonzero(gen_is_dc)
if len(dc_gen):
self.Qg_max[dc_gen] = 0.0
self.Qg_min[dc_gen] = 0.0
# --------------------------------------------------------------------------------------
# Check the active elements and their operational limits.
self.nbr = nc.passive_branch_data.nelm
self.br_idx = np.arange(self.nbr)
self.br_mon_idx = nc.passive_branch_data.get_monitor_enabled_indices()
self.gen_disp_idx = nc.generator_data.get_dispatchable_indices()
self.gen_disp_idx_sh = np.r_[self.gen_disp_idx, np.arange(self.ngen, self.ngen + self.nsh)]
self.Cfmon = nc.passive_branch_data.monitored_Cf(self.br_mon_idx)
self.Cfmon_t = self.Cfmon.T
self.Ctmon = nc.passive_branch_data.monitored_Ct(self.br_mon_idx)
self.Ctmon_t = self.Ctmon.T
self.R = nc.passive_branch_data.R
self.X = nc.passive_branch_data.X
# Sizing of the problem
self.nbus = nc.bus_data.nbus
self.n_slack = len(self.slack)
self.ntapm = len(self.k_m)
self.ntapt = len(self.k_tau)
self.npv = len(self.pv)
self.npq = len(self.pq)
self.n_br_mon = len(self.br_mon_idx)
self.n_gen_disp = len(self.gen_disp_idx)
self.n_gen_disp_sh = self.n_gen_disp + self.nsh
self.ind_gens = np.arange(len(self.Pg_max))
self.gen_nondisp_idx = nc.generator_data.get_non_dispatchable_indices()
self.gen_bus_idx = np.r_[self.nc.generator_data.get_bus_indices(), self.sh_bus_idx]
self.Sg_undis = (nc.generator_data.get_injections() / self.Sbase)[self.gen_nondisp_idx]
self.rates = nc.passive_branch_data.rates / self.Sbase # Line loading limits. If the grid is not well conditioned, add constant value (i.e. +100)
self.rates2 = np.power(self.rates[self.br_mon_idx], 2.0)
self.Va_max = nc.bus_data.angle_max # This limits are not really used as of right now.
self.Va_min = nc.bus_data.angle_min
# Relevant ids
self.Ybus_indptr = self.admittances.Ybus.indptr
self.Ybus_cols, self.Ybus_indices = self.admittances.Ybus.nonzero()
# positions of the diagonal in the CSC scheme
self.Ybus_diag_pos = np.where(self.Ybus_indices == self.Ybus_cols)[0]
# TODO: Maybe an opportunity to use some sort of function so this is clearer?
self.Cdispgen = csc_matrix((np.ones(self.n_gen_disp),
(self.gen_bus_idx[self.gen_disp_idx],
np.arange(self.n_gen_disp))),
shape=(self.nbus, self.n_gen_disp))
self.Cdispgen_t = self.Cdispgen.T
self.Cdispgen_sh = csc_matrix((np.ones(self.n_gen_disp_sh),
(self.gen_bus_idx[self.gen_disp_idx_sh],
np.arange(self.n_gen_disp_sh))),
shape=(self.nbus, self.n_gen_disp_sh))
self.Cdispgen_sh_t = self.Cdispgen.T
self.Inom = nc.generator_data.snom[self.gen_disp_idx] / self.Sbase
self.c0 = np.r_[nc.generator_data.cost_0[self.gen_disp_idx], np.zeros(self.nsh)]
self.c1 = np.r_[nc.generator_data.cost_1[self.gen_disp_idx], np.zeros(self.nsh)]
self.c2 = np.r_[nc.generator_data.cost_2[self.gen_disp_idx], np.zeros(self.nsh)]
self.c0n = nc.generator_data.cost_0[self.gen_nondisp_idx]
self.c1n = nc.generator_data.cost_1[self.gen_nondisp_idx]
self.c2n = nc.generator_data.cost_2[self.gen_nondisp_idx]
# Transformer operational limits
self.tapm_max = nc.active_branch_data.tap_module_max[self.k_m]
self.tapm_min = nc.active_branch_data.tap_module_min[self.k_m]
self.tapt_max = nc.active_branch_data.tap_angle_max[self.k_tau]
self.tapt_min = nc.active_branch_data.tap_angle_min[self.k_tau]
# We grab all tapm even when uncontrolled since the indexing is needed
self.all_tap_m = nc.active_branch_data.tap_module
# if the tapt of the same trafo is variable.
# We grab all tapt even when uncontrolled since the indexing is needed if
self.all_tap_tau = nc.active_branch_data.tap_angle
# the tapm of the same trafo is variable.
# TODO: Simplify this using a method in the hvdc_data class
self.hvdc_nondisp_idx = np.where(nc.hvdc_data.dispatchable == 0)[0]
self.hvdc_disp_idx = np.where(nc.hvdc_data.dispatchable == 1)[0]
self.f_nd_hvdc = nc.hvdc_data.F[self.hvdc_nondisp_idx]
self.t_nd_hvdc = nc.hvdc_data.T[self.hvdc_nondisp_idx]
self.Pf_nondisp = nc.hvdc_data.Pset[self.hvdc_nondisp_idx]
self.n_disp_hvdc = len(self.hvdc_disp_idx)
self.f_disp_hvdc = nc.hvdc_data.F[self.hvdc_disp_idx]
self.t_disp_hvdc = nc.hvdc_data.T[self.hvdc_disp_idx]
self.P_hvdc_max = nc.hvdc_data.rates[self.hvdc_disp_idx]
if options.acopf_mode == AcOpfMode.ACOPFslacks:
self.nsl = 2 * self.npq + 2 * self.n_br_mon
# Slack relaxations for constraints
# Cost squared since the slack is also squared
self.c_s = np.power(nc.passive_branch_data.overload_cost[self.br_mon_idx] + 0.1, 1.0) # TODO power of 1???
self.c_v = nc.bus_data.cost_v[self.pq] + 0.1
else:
self.nsl = 0
self.c_s = np.zeros(0)
self.c_v = np.zeros(0)
if optimize_nodal_capacity:
self.nslcap = len(capacity_nodes_idx)
self.slcap0 = np.zeros(self.nslcap)
else:
self.nslcap = 0
self.slcap0 = np.zeros(0)
# +2*nvsc equalities: VSC loss + current-definition
self.neq = 2 * self.nbus + self.n_slack + self.npv + 2 * self.nvsc
# +1*nvsc (It >= 0) inequality
# +1*n_vsc_lim (It <= rate/Sbase) inequality
# +1*n_sl_vsc (sl_vsc >= 0) inequality
n_vsc_ineq = self.nvsc + self.n_vsc_lim + self.n_sl_vsc
if options.ips_control_q_limits:
self.nineq = (2 * self.n_br_mon + 2 * self.npq + self.n_gen_disp + 4 * self.n_gen_disp_sh + 2 * self.ntapm
+ 2 * self.ntapt + 2 * self.n_disp_hvdc + self.nsl + self.nslcap + n_vsc_ineq)
else:
# No Reactive constraint (power curve)
self.nineq = (2 * self.n_br_mon + 2 * self.npq + 4 * self.n_gen_disp_sh + 2 * self.ntapm + 2 * self.ntapt
+ 2 * self.n_disp_hvdc + self.nsl + self.nslcap + n_vsc_ineq)
# Variables
if pf_init:
# TODO: try to substitute by using nc.generator_data.get_injections_per_bus()
# @Carlos: get_injections does not account for the powerflow results
# This array has the number of total generators connected to the same bus of each generator, counting itself
ngenforgen = np.bincount(self.gen_bus_idx[:self.ngen])[self.gen_bus_idx[:self.ngen]]
# If there are multiple generators connected to the same bus, they share in equal parts the injection.
allPgen = (Sbus_pf.real + self.Sd.real)[self.gen_bus_idx[:self.ngen]] / ngenforgen
# Same for Q
allQgen = (Sbus_pf.imag + self.Sd.imag)[self.gen_bus_idx[:self.ngen]] / ngenforgen
self.Sg_undis = allPgen[self.gen_nondisp_idx] + 1j * allQgen[self.gen_nondisp_idx]
self.Pg = np.r_[allPgen[self.gen_disp_idx], np.zeros(self.nsh)]
self.Qg = np.r_[allQgen[self.gen_disp_idx], np.zeros(self.nsh)]
self.Vm = np.abs(voltage_pf)
self.Va = np.angle(voltage_pf)
self.tap_m = nc.active_branch_data.tap_module[self.k_m]
self.tap_tau = nc.active_branch_data.tap_angle[self.k_tau]
self.Pfdc = nc.hvdc_data.Pset[self.hvdc_disp_idx]
else:
# TODO: Review this
# Pmax = nc.generator_data.pmax / self.Sbase
# Pmin = nc.generator_data.pmin / self.Sbase
self.Pg = np.r_[
# (Pmax[gen_disp_idx_2] + Pmin[gen_disp_idx_2]) / 2,
(self.Pg_max[self.gen_disp_idx] + self.Pg_min[self.gen_disp_idx]) / 2,
np.zeros(self.nsh)
]
self.Qg = np.r_[
(self.Qg_max[self.gen_disp_idx] + self.Qg_min[self.gen_disp_idx]) / 2,
np.zeros(self.nsh)
]
self.Va = np.angle(nc.bus_data.Vbus)
self.Vm = (self.Vm_max + self.Vm_min) / 2
self.tap_m = nc.active_branch_data.tap_module[self.k_m]
self.tap_tau = nc.active_branch_data.tap_angle[self.k_tau]
self.Pfdc = np.zeros(self.n_disp_hvdc)
if options.acopf_mode == AcOpfMode.ACOPFslacks:
self.sl_sf = np.ones(self.n_br_mon)
self.sl_st = np.ones(self.n_br_mon)
self.sl_vmax = np.ones(self.npq)
self.sl_vmin = np.ones(self.npq)
else:
self.sl_sf = np.zeros(0)
self.sl_st = np.zeros(0)
self.sl_vmax = np.zeros(0)
self.sl_vmin = np.zeros(0)
if optimize_nodal_capacity:
self.slcap = np.zeros(self.nslcap)
else:
self.slcap = np.zeros(0)
# AC/DC + VSC extension: initialise the new VSC state and pin DC bus angles
# DC bus voltage angle is meaningless and is fixed to 0 by an equality constraint
if self.n_dc_bus:
self.Va[self.dc_bus_idx] = 0.0
# A strictly-positive It keeps the subblock Jacobian way from singularity
self.Pt_vsc = np.zeros(self.nvsc)
self.Qt_vsc = np.zeros(self.nvsc)
self.It_vsc = np.full(self.nvsc, 0.1)
self.Pf_vsc = (self.vsc_alpha1
+ self.vsc_alpha2 * self.It_vsc
+ self.vsc_alpha3 * self.It_vsc * self.It_vsc)
# sl_vsc only exists in ACOPFslacks mode (mirrors the sl_sf/... block above)
if options.acopf_mode == AcOpfMode.ACOPFslacks:
self.sl_vsc = np.ones(self.n_sl_vsc)
else:
self.sl_vsc = np.zeros(0)
# ------------------------------------------------------------------------------------
self.x0 = self.var2x()
self.NV = len(self.x0)
# Predefine some global electrical variable
self.V = polar_to_rect(self.Vm, self.Va)
self.Scalc = compute_power(self.admittances.Ybus, self.V)
self.allSf = get_Sf(k=self.br_idx, Vm=self.Vm, V=self.V,
yff=self.admittances.yff, yft=self.admittances.yft,
F=self.from_idx, T=self.to_idx)
self.allSt = get_St(k=self.br_idx, Vm=self.Vm, V=self.V,
ytf=self.admittances.ytf, ytt=self.admittances.ytt,
F=self.from_idx, T=self.to_idx)
self.Sf = self.allSf[self.br_mon_idx]
self.St = self.allSt[self.br_mon_idx]
self.Sf2 = np.conj(self.Sf) * self.Sf
self.St2 = np.conj(self.St) * self.St
[docs]
def analyze_branch_controls(self) -> None:
"""
Analyze the control branches and compute the indices
:return: None
"""
k_pf_tau = list()
k_pt_tau = list()
k_qf_m = list()
k_qt_m = list()
k_v_m = list()
for k in range(self.nc.nbr):
ctrl_m = self.nc.active_branch_data.tap_module_control_mode[k]
ctrl_tau = self.nc.active_branch_data.tap_phase_control_mode[k]
# analyze tap-module controls
if ctrl_m == TapModuleControl.Vm.idx():
# In any other case, the voltage is managed by the tap module
k_v_m.append(k)
elif ctrl_m == TapModuleControl.Qf.idx():
k_qf_m.append(k)
elif ctrl_m == TapModuleControl.Qt.idx():
k_qt_m.append(k)
elif ctrl_m == TapModuleControl.fixed.idx():
pass
else:
raise Exception(f"Unknown tap phase module mode {ctrl_m}")
# analyze tap-phase controls
if ctrl_tau == TapPhaseControl.Pf.idx():
k_pf_tau.append(k)
elif ctrl_tau == TapPhaseControl.Pt.idx():
k_pt_tau.append(k)
elif ctrl_tau == TapPhaseControl.fixed.idx():
if ctrl_m == TapModuleControl.fixed.idx():
conv_type = 1
# elif ctrl_tau == TapPhaseControl.Droop:
# pass
elif ctrl_tau == 0:
pass
else:
raise Exception(f"Unknown tap phase control mode {ctrl_tau}")
# convert lists to integer arrays
k_pf_tau = np.array(k_pf_tau, dtype=int)
k_pt_tau = np.array(k_pt_tau, dtype=int)
k_qf_m = np.array(k_qf_m, dtype=int)
k_qt_m = np.array(k_qt_m, dtype=int)
k_v_m = np.array(k_v_m, dtype=int)
self.k_m = np.r_[k_v_m, k_qf_m, k_qt_m]
self.k_tau = np.r_[k_pf_tau, k_pt_tau]
self.k_mtau = np.intersect1d(self.k_m, self.k_tau)
[docs]
def var2x(self) -> Vec:
return np.r_[
self.Va,
self.Vm,
self.Pg,
self.Qg,
self.sl_sf,
self.sl_st,
self.sl_vmax,
self.sl_vmin,
self.slcap,
self.tap_m,
self.tap_tau,
self.Pfdc,
self.Pt_vsc,
self.Qt_vsc,
self.Pf_vsc,
self.It_vsc,
self.sl_vsc,
]
[docs]
def x2var(self, x: Vec):
a = 0
b = self.nbus
self.Va = x[a: b]
a = b
b += self.nbus
self.Vm = x[a: b]
a = b
b += self.n_gen_disp_sh
self.Pg = x[a: b]
a = b
b += self.n_gen_disp_sh
self.Qg = x[a: b]
a = b
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
b += self.n_br_mon
self.sl_sf = x[a: b]
a = b
b += self.n_br_mon
self.sl_st = x[a: b]
a = b
b += self.npq
self.sl_vmax = x[a: b]
a = b
b += self.npq
self.sl_vmin = x[a: b]
a = b
b += self.nslcap
else:
b += self.nslcap
# Create empty arrays for not used variables
self.sl_sf = np.zeros(0)
self.sl_st = np.zeros(0)
self.sl_vmax = np.zeros(0)
self.sl_vmin = np.zeros(0)
self.slcap = x[a:b]
a = b
b += self.ntapm
self.tap_m = x[a: b]
a = b
b += self.ntapt
self.tap_tau = x[a: b]
a = b
b += self.n_disp_hvdc
self.Pfdc = x[a: b]
a = b
b += self.nvsc
self.Pt_vsc = x[a: b]
a = b
b += self.nvsc
self.Qt_vsc = x[a: b]
a = b
b += self.nvsc
self.Pf_vsc = x[a: b]
a = b
b += self.nvsc
self.It_vsc = x[a: b]
a = b
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
b += self.n_sl_vsc
self.sl_vsc = x[a: b]
else:
self.sl_vsc = np.zeros(0)
[docs]
def update(self, x: Vec) -> Tuple[Vec, Vec, Vec]:
# Set the new values for the variables.
self.x2var(x)
# Update the admittances matrices in case there are changes to the taps
if self.ntapm + self.ntapt != 0:
prev_all_tap_m = self.all_tap_m.copy()
prev_all_tap_tau = self.all_tap_tau.copy()
self.all_tap_m[self.k_m] = self.tap_m
self.all_tap_tau[self.k_tau] = self.tap_tau
self.admittances.modify_taps_all(m=prev_all_tap_m,
m2=self.all_tap_m,
tau=prev_all_tap_tau,
tau2=self.all_tap_tau)
else:
pass
# Compute the electrical balance
self.V = polar_to_rect(self.Vm, self.Va)
self.Scalc = compute_power(self.admittances.Ybus, self.V)
Pgen = np.zeros(self.nbus)
Qgen = np.zeros(self.nbus)
np.add.at(Pgen, self.gen_bus_idx[self.gen_disp_idx_sh], self.Pg) # Variable generation
np.add.at(Pgen, self.gen_bus_idx[self.gen_nondisp_idx], self.Sg_undis.real) # Variable generation
np.add.at(Qgen, self.gen_bus_idx[self.gen_disp_idx_sh], self.Qg) # Fixed generation
np.add.at(Qgen, self.gen_bus_idx[self.gen_nondisp_idx], self.Sg_undis.imag) # Fixed generation
dS = self.Scalc + self.Sd - Pgen - 1j * Qgen # Nodal power balance
if self.nslcap != 0:
dS[self.capacity_nodes_idx] -= self.slcap # Nodal capacity slack generator addition
for link in range(self.n_disp_hvdc):
dS[self.f_disp_hvdc[link]] += self.Pfdc[link] # Variable DC links. Lossless model (Pdc_From = Pdc_To)
dS[self.t_disp_hvdc[link]] -= self.Pfdc[link]
for nd_link in range(len(self.hvdc_nondisp_idx)):
dS[self.f_nd_hvdc[nd_link]] += self.Pf_nondisp[nd_link] # Fixed DC links
dS[self.t_nd_hvdc[nd_link]] -= self.Pf_nondisp[nd_link]
# VSC contribution to the nodal balance
for k in range(self.nvsc):
dS[self.T_vsc[k]] += self.Pt_vsc[k] + 1j * self.Qt_vsc[k] # AC bus
dS[self.F_vsc[k]] += self.Pf_vsc[k] # DC+ bus
self.allSf = get_Sf(k=self.br_idx, Vm=self.Vm, V=self.V,
yff=self.admittances.yff, yft=self.admittances.yft,
F=self.from_idx, T=self.to_idx, )
self.allSt = get_St(k=self.br_idx, Vm=self.Vm, V=self.V,
ytf=self.admittances.ytf, ytt=self.admittances.ytt,
F=self.from_idx, T=self.to_idx, )
self.Sf = self.allSf[self.br_mon_idx]
self.St = self.allSt[self.br_mon_idx]
self.Sf2 = np.conj(self.Sf) * self.Sf
self.St2 = np.conj(self.St) * self.St
fval = 1e-4 * (
np.sum((self.c0 + self.c1 * self.Pg * self.Sbase + self.c2 * np.power(self.Pg * self.Sbase, 2)))
+ np.sum(self.c_s * (self.sl_sf + self.sl_st)) + np.sum(self.c_v * (self.sl_vmax + self.sl_vmin))
+ np.sum(self.c_vsc * self.sl_vsc)
+ np.sum(self.nodal_capacity_sign * self.slcap))
# DC buses have no meaningful voltage angle and their imaginary
# nodal balance row is structurally 0 == 0, which would make the KKT
# singular. Replace that degenerate row with the constraint Va[dc] = 0
g_imag = dS.imag.copy()
if self.n_dc_bus:
g_imag[self.dc_bus_idx] = self.Va[self.dc_bus_idx]
# VSC equality constraints (It auxiliary-variable formulation):
# loss: a1 + a2*It + a3*It^2 - Pt - Pf = 0 (== pf_generalized loss_vsc)
# curdef: It^2 * Vm[T]^2 - (Pt^2 + Qt^2) = 0 (It = sqrt(Pt^2+Qt^2)/Vm[T])
if self.nvsc:
loss_vsc = (self.vsc_alpha1
+ self.vsc_alpha2 * self.It_vsc
+ self.vsc_alpha3 * self.It_vsc * self.It_vsc
- self.Pt_vsc - self.Pf_vsc)
vm_t = self.Vm[self.T_vsc]
curdef_vsc = (self.It_vsc * self.It_vsc * vm_t * vm_t
- self.Pt_vsc * self.Pt_vsc - self.Qt_vsc * self.Qt_vsc)
else:
loss_vsc = np.zeros(0)
curdef_vsc = np.zeros(0)
gval = np.r_[dS.real, g_imag, self.Va[self.slack],
self.Vm[self.pv] - self.Vm_max[self.pv],
loss_vsc, curdef_vsc]
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
sl_sf = self.sl_sf
sl_st = self.sl_st
sl_vmax = self.sl_vmax
sl_vmin = self.sl_vmin
else:
sl_sf = np.zeros(self.n_br_mon)
sl_st = np.zeros(self.n_br_mon)
sl_vmax = np.zeros(self.npq)
sl_vmin = np.zeros(self.npq)
if self.options.ips_control_q_limits: # if reactive power control...
v_g = self.Vm[self.gen_bus_idx[self.gen_disp_idx]]
ctrlq_ineq = (np.power(self.Qg[:self.n_gen_disp], 2.0)
+ np.power(self.Pg[:self.n_gen_disp], 2.0)
- np.power(v_g * self.Inom, 2.0))
else:
ctrlq_ineq = np.zeros(0)
if self.n_disp_hvdc != 0:
hvdc_ineq1 = self.Pfdc - self.P_hvdc_max
hvdc_ineq2 = - self.P_hvdc_max - self.Pfdc
else:
hvdc_ineq1 = np.zeros(0)
hvdc_ineq2 = np.zeros(0)
# Nodal capacity sign constraint: bound slcap to the right direction
# sign > 0 (maximize generation): slcap <= 0 -> slcap <= 0
# sign < 0 (maximize load): slcap >= 0 -> -slcap <= 0
if self.nslcap != 0:
slcap_ineq = np.sign(self.nodal_capacity_sign) * self.slcap
else:
slcap_ineq = np.zeros(0)
# VSC inequalities (appended at the very end of hval; Hx rows must follow this order):
# It >= 0 -> -It <= 0
# It <= rate/Sbase
if self.nvsc:
vsc_it_pos = - self.It_vsc
vsc_cur_lim = self.It_vsc[self.vsc_lim_idx] - self.vsc_rate_pu[self.vsc_lim_idx]
if self.n_sl_vsc:
vsc_cur_lim = vsc_cur_lim - self.sl_vsc
vsc_sl_pos = - self.sl_vsc
else:
vsc_sl_pos = np.zeros(0)
else:
vsc_it_pos = np.zeros(0)
vsc_cur_lim = np.zeros(0)
vsc_sl_pos = np.zeros(0)
hval = np.r_[
self.Sf2.real - self.rates2 - sl_sf, # rates "lower limit"
self.St2.real - self.rates2 - sl_st, # rates "upper limit"
self.Vm[self.pq] - self.Vm_max[self.pq] - sl_vmax, # voltage module upper limit
self.Pg - self.Pg_max[self.gen_disp_idx_sh], # generator P upper limits
self.Qg - self.Qg_max[self.gen_disp_idx_sh], # generator Q upper limits
self.Vm_min[self.pq] - self.Vm[self.pq] - sl_vmin, # voltage module lower limit
self.Pg_min[self.gen_disp_idx_sh] - self.Pg, # generator P lower limits
self.Qg_min[self.gen_disp_idx_sh] - self.Qg, # generation Q lower limits
- self.sl_sf, # Slack variable for Sf >0
- self.sl_st, # Slack variable for St >0
- self.sl_vmax, # Slack variable for Vmax >0
- self.sl_vmin, # Slack variable for Vmin >0
self.tap_m - self.tapm_max, # Tap module upper bound
self.tapm_min - self.tap_m, # Tap module lower bound
self.tap_tau - self.tapt_max, # Tap module lower bound
self.tapt_min - self.tap_tau, # Tap phase lower bound
ctrlq_ineq,
hvdc_ineq1,
hvdc_ineq2,
slcap_ineq, # Nodal capacity sign bound
vsc_it_pos, # VSC: It >= 0
vsc_cur_lim, # VSC: AC current limit
vsc_sl_pos, # VSC: sl_vsc >= 0 (ACOPFslacks only)
]
return fval, gval, hval
[docs]
def get_jacobians_and_hessians(self, mu: Vec, lam: Vec, compute_hessians: bool) -> Tuple[
Vec, csc_matrix, csc_matrix,
csc_matrix, csc_matrix, csc_matrix]:
"""
TODO: we should split this function into functions outside the class, that should make it more manageable
one for each of these: fx, Gx, Hx, fxx, Gxx, Hxx, and leave this function just to call them
:param mu:
:param lam:
:param compute_hessians:
:return: fx, Gx, Hx, fxx, Gxx, Hxx
"""
# The DC-bus imaginary nodal balance rows were replaced by the linear
# constraint Va[dc]=0, which results in a zero Hessian.
# Thus, make their multipliers zero
if compute_hessians and self.n_dc_bus:
lam = np.array(lam, dtype=float, copy=True)
lam[self.nbus + self.dc_bus_idx] = 0.0
# Number of variables of the typical power flow (V, th, P, Q). Used to ease readability
npfvar = 2 * self.nbus + 2 * self.n_gen_disp_sh
# AC/DC + VSC extension
nvc = self.n_vsc_vars
_vsc_k = np.arange(self.nvsc, dtype=int)
vsc_col0 = (npfvar + self.nsl + self.nslcap
+ self.ntapm + self.ntapt + self.n_disp_hvdc)
col_Pt = vsc_col0 + _vsc_k
col_Qt = vsc_col0 + self.nvsc + _vsc_k
col_Pf = vsc_col0 + 2 * self.nvsc + _vsc_k
col_It = vsc_col0 + 3 * self.nvsc + _vsc_k
col_sl_vsc = vsc_col0 + 4 * self.nvsc + np.arange(self.n_sl_vsc, dtype=int)
if self.options.ips_control_q_limits: # if reactive power control...
nqct = self.n_gen_disp
else:
nqct = 0
Vmat = diags(self.V)
vm_inv = diags(1 / self.Vm)
E = Vmat @ vm_inv
Ibus = self.admittances.Ybus @ self.V
self.all_tap_m[self.k_m] = self.tap_m
self.all_tap_tau[self.k_tau] = self.tap_tau
# Useful pre-constructed matrices:
diags_gensh_disp_ones = diags(np.ones(self.n_gen_disp_sh))
diags_bus_ones = diags(np.ones(self.nbus))
diags_pq_ones = diags(np.ones(self.npq))
diags_disp_hvdc_ones = diags(np.ones(self.n_disp_hvdc))
diags_br_mon_ones = diags(np.ones(self.n_br_mon))
# OBJECTIVE FUNCTION GRAD --------------------------------------------------------------------------------------
ts_fx = timeit.default_timer()
fx = np.zeros(self.NV)
if self.nslcap == 0:
fx[2 * self.nbus: 2 * self.nbus + self.n_gen_disp_sh] = (2 * self.c2 * self.Pg * (self.Sbase * self.Sbase)
+ self.c1 * self.Sbase) * 1e-4
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
fx[npfvar: npfvar + self.n_br_mon] = self.c_s
fx[npfvar + self.n_br_mon: npfvar + 2 * self.n_br_mon] = self.c_s
fx[npfvar + 2 * self.n_br_mon: npfvar + 2 * self.n_br_mon + self.npq] = self.c_v
fx[npfvar + 2 * self.n_br_mon + self.npq: npfvar + 2 * self.n_br_mon + 2 * self.npq] = self.c_v
else:
fx[npfvar + self.nsl: npfvar + self.nsl + self.nslcap] = self.nodal_capacity_sign
# VSC current-limit slack penalty (objective is scaled by 1e-4)
if self.n_sl_vsc:
fx[col_sl_vsc] = 1e-4 * self.c_vsc
te_fx = timeit.default_timer()
# EQUALITY CONSTRAINTS GRAD ------------------------------------------------------------------------------------
"""
The following comments illustrate the shapes of the equality constraints gradients.
Gx =
NV
+----------+
| GS.real | N
+----------+
| GS.imag | N (DC-bus rows = Va[dc] = 0)
+----------+
| GTH | nslack
+----------+
| Gvm | npv
+----------+
| G_loss | nvsc a1 + a2*It + a3*It^2 - Pt - Pf = 0
+----------+
| G_curdef | nvsc It^2*Vm[T]^2 - Pt^2 - Qt^2 = 0
+----------+
where Gx has shape
(N + N + nslack + npv + nvsc + nvsc,
N + N + Ng + Ng + nsl + nslcap + ntapm + ntapt + ndc
+ 4*nvsc + n_sl_vsc),
nslack is the number of slack buses and nsl the number of AC slack
variables.
We also add the set of columns vsc = Pt|Qt|Pf|It|slV
GS =
N N Ng Ng nsl slc tapm tapt ndc vsc
+------+-----+-----+-----+-----+-----+-----+-----+-----+---------------+
| GSva | GSvm| GSpg| GSqg| Gsl | Gslc| Gtm | Gtt |GSpfdc| GSpt GSqt | N
| | | | | | | | | | GSpf GSit Gslv|
+------+-----+-----+-----+-----+-----+-----+-----+-----+---------------+
GTH =
N N Ng Ng nsl slc tapm tapt ndc vsc
+-----+---+----+----+----+----+----+----+----+-----+
| GTHx|0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | (1 at Va[slack])
+-----+---+----+----+----+----+----+----+----+-----+
Gvm =
N N Ng Ng nsl slc tapm tapt ndc vsc
+----+-----+----+----+----+----+----+----+----+-----+
| 0 | Gvmx| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | (1 at Vm[pv])
+----+-----+----+----+----+----+----+----+----+-----+
G_loss =
N N Ng Ng nsl slc tapm tapt ndc vsc
+----+----+----+----+----+----+----+----+----+-----+
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Gl | nvsc
+----+----+----+----+----+----+----+----+----+-----+
Gl: -1 @Pt, -1 @Pf, (a2 + 2*a3*It) @It, 0 @slV
G_curdef =
N N Ng Ng nsl slc tapm tapt ndc vsc
+----+----+----+----+----+----+----+----+----+-----+
| 0 | Gc | 0 | 0 | 0 | 0 | 0 | 0 | 0 | Gc2 | nvsc
+----+----+----+----+----+----+----+----+----+-----+
Gc : 2*It^2*Vm[T] (at the Vm[T] column of the Vm group)
Gc2: -2*Pt @Pt, -2*Qt @Qt, 2*It*Vm[T]^2 @It, 0 @slV
"""
ts_gx = timeit.default_timer()
dataYbus_Vmat = self.admittances.Ybus.data * self.V[self.Ybus_cols]
# GSvm = Vmat @ (np.conj(diags(Ibus)) + np.conj(self.admittances.Ybus @ Vmat)) @ vm_inv # N x N matrix
data = dataYbus_Vmat.copy()
np.add.at(data, self.Ybus_diag_pos, Ibus)
GSvm = csc_matrix((np.conj(data) * (1 / self.Vm)[self.Ybus_cols]
* self.V[self.Ybus_indices], self.Ybus_indices, self.Ybus_indptr),
shape=(self.nbus, self.nbus))
# GSva = 1j * Vmat @ (np.conj(diags(Ibus)) - np.conj(self.admittances.Ybus @ Vmat))
data = - dataYbus_Vmat.copy()
np.add.at(data, self.Ybus_diag_pos, Ibus)
GSva = csc_matrix((np.conj(data) * 1j * self.V[self.Ybus_indices], self.Ybus_indices, self.Ybus_indptr),
shape=(self.nbus, self.nbus))
GSpg = - self.Cdispgen_sh
GSqg = -1j * self.Cdispgen_sh
GTH = lil_matrix((len(self.slack), self.NV))
for i, ss in enumerate(self.slack):
GTH[i, ss] = 1.
Gvm = lil_matrix((len(self.pv), self.NV))
for i, ss in enumerate(self.pv):
Gvm[i, self.nbus + ss] = 1.
dSbusdm, dSfdm, dStdm, dSbusdt, dSfdt, dStdt = self.compute_branch_power_derivatives()
if self.ntapm > 0:
Gtapm = dSbusdm
else:
Gtapm = csc((self.nbus, self.ntapm), dtype=complex)
if self.ntapt > 0:
Gtapt = dSbusdt
else:
Gtapt = csc((self.nbus, self.ntapt), dtype=complex)
GSpfdc = lil_matrix((self.nbus, self.n_disp_hvdc), dtype=complex)
for k_link in range(self.n_disp_hvdc):
GSpfdc[self.f_disp_hvdc[k_link], k_link] = 1.0 # TODO: check that this is correct
GSpfdc[self.t_disp_hvdc[k_link], k_link] = -1.0 # TODO: check that this is correct
Gslack = csc((self.nbus, self.nsl), dtype=complex)
Gslcap = lil_matrix((self.nbus, self.nslcap), dtype=complex)
if self.nslcap != 0:
for idslcap, capbus in enumerate(self.capacity_nodes_idx):
Gslcap[capbus, idslcap] = -1
# VSC nodal-injection columns (same role as GSpfdc for the HVDC links):
# dS[T] += Pt + 1j*Qt (AC bus)
# dS[F] += Pf (DC+ bus)
GSpt = csc_matrix((np.ones(self.nvsc, dtype=complex), (self.T_vsc, _vsc_k)),
shape=(self.nbus, self.nvsc))
GSqt = csc_matrix((1j * np.ones(self.nvsc), (self.T_vsc, _vsc_k)),
shape=(self.nbus, self.nvsc))
GSpf = csc_matrix((np.ones(self.nvsc, dtype=complex), (self.F_vsc, _vsc_k)),
shape=(self.nbus, self.nvsc))
GSit = csc((self.nbus, self.nvsc), dtype=complex)
GSslvsc = csc((self.nbus, self.n_sl_vsc), dtype=complex)
GS = sp.hstack([GSva, GSvm, GSpg, GSqg, Gslack, Gslcap, Gtapm, Gtapt, GSpfdc,
GSpt, GSqt, GSpf, GSit, GSslvsc])
GS_real = GS.real
GS_imag = GS.imag
# DC buses have no meaningful angle: replace their (degenerate) imaginary
# nodal-balance row with the linear constraint Va[dc] = 0.
if self.n_dc_bus:
keep = np.ones(self.nbus)
keep[self.dc_bus_idx] = 0.0
GS_imag = sp.diags(keep) @ GS_imag
GS_imag = GS_imag + csc_matrix(
(np.ones(self.n_dc_bus), (self.dc_bus_idx, self.dc_bus_idx)),
shape=(self.nbus, self.NV))
# VSC equality rows: loss and current Ilim definition (curdef)
# loss = a1 + a2*It + a3*It^2 - Pt - Pf
# curdef = It^2*Vm[T]^2 - Pt^2 - Qt^2
vm_t = self.Vm[self.T_vsc]
# Form triplets (row, column, value)
loss_r = np.concatenate([_vsc_k, _vsc_k, _vsc_k])
loss_c = np.concatenate([col_Pt, col_Pf, col_It])
loss_v = np.concatenate([-np.ones(self.nvsc), -np.ones(self.nvsc),
self.vsc_alpha2 + 2.0 * self.vsc_alpha3 * self.It_vsc])
G_loss = csc_matrix((loss_v, (loss_r, loss_c)), shape=(self.nvsc, self.NV))
curd_r = np.concatenate([_vsc_k, _vsc_k, _vsc_k, _vsc_k])
curd_c = np.concatenate([self.nbus + self.T_vsc, col_Pt, col_Qt, col_It])
curd_v = np.concatenate([2.0 * self.It_vsc ** 2 * vm_t,
-2.0 * self.Pt_vsc,
-2.0 * self.Qt_vsc,
2.0 * self.It_vsc * vm_t ** 2])
G_curdef = csc_matrix((curd_v, (curd_r, curd_c)), shape=(self.nvsc, self.NV))
Gx = sp.vstack([GS_real, GS_imag, GTH, Gvm, G_loss, G_curdef]).tocsc()
te_gx = timeit.default_timer()
# INEQUALITY CONSTRAINTS GRAD ----------------------------------------------------------------------------------
"""
The following comments illustrate the shapes of the inequality
constraints gradient
Hx =
NV
+---------+
| HSf | M
+---------+
| HSt | M
+---------+
| Hvu | N
+---------+
| Hpu | Ng
+---------+
| Hqu | Ng
+---------+
| Hvl | N
+---------+
| Hpl | Ng
+---------+
| Hql | Ng
+---------+
| Hslsf | M
+---------+
| Hslst | M
+---------+
| Hslvmax | npq
+---------+
| Hslvmin | npq
+---------+
| Htapmu | ntapm
+---------+
| Htapml | ntapm
+---------+
| Htaptu | ntapt
+---------+
| Htaptl | ntapt
+---------+
| Hqmax | Ng (if ctQ==True), 0 else
+---------+
| Hdcu | ndc
+---------+
| Hdcl | ndc
+---------+
| Hslcap | nslcap
+---------+
| Hvscit | nvsc -It <= 0
+---------+
| Hvsccur | n_vsc_lim It - rate/Sb (- slV) <= 0
+---------+
| Hvscsl | n_sl_vsc
+---------+
"""
ts_hx = timeit.default_timer()
Yf_mon = self.admittances.Yf[self.br_mon_idx, :]
Yt_mon = self.admittances.Yt[self.br_mon_idx, :]
Vfmat = diags(self.V[self.nc.passive_branch_data.F[self.br_mon_idx]])
Vtmat = diags(self.V[self.nc.passive_branch_data.T[self.br_mon_idx]])
IfCJmat = np.conj(diags(Yf_mon @ self.V))
ItCJmat = np.conj(diags(Yt_mon @ self.V))
Sfmat = diags(self.Sf)
Stmat = diags(self.St)
# thisfitswell_Cf@E = csc(((self.V / self.Vm)[self.from_idx[self.br_mon_idx]], (self.br_idx[self.br_mon_idx],self.from_idx[self.br_mon_idx])), shape = (self.n_br_mon, self.nbus))
Sfvm = IfCJmat @ self.Cfmon @ E + Vfmat @ np.conj(Yf_mon @ E)
Stvm = ItCJmat @ self.Ctmon @ E + Vtmat @ np.conj(Yt_mon @ E)
Sfva = 1j * (IfCJmat @ self.Cfmon @ Vmat - Vfmat @ np.conj(Yf_mon @ Vmat))
Stva = 1j * (ItCJmat @ self.Ctmon @ Vmat - Vtmat @ np.conj(Yt_mon @ Vmat))
Hpu = sp.hstack([lil_matrix((self.n_gen_disp_sh, 2 * self.nbus)), diags_gensh_disp_ones,
lil_matrix((self.n_gen_disp_sh, self.NV - 2 * self.nbus - self.n_gen_disp_sh))])
Hpl = sp.hstack([lil_matrix((self.n_gen_disp_sh, 2 * self.nbus)), - diags_gensh_disp_ones,
lil_matrix((self.n_gen_disp_sh, self.NV - 2 * self.nbus - self.n_gen_disp_sh))])
Hqu = sp.hstack(
[lil_matrix((self.n_gen_disp_sh, 2 * self.nbus + self.n_gen_disp_sh)), diags_gensh_disp_ones,
lil_matrix((self.n_gen_disp_sh, self.NV - 2 * self.nbus - 2 * self.n_gen_disp_sh))])
Hql = sp.hstack(
[lil_matrix((self.n_gen_disp_sh, 2 * self.nbus + self.n_gen_disp_sh)), - diags_gensh_disp_ones,
lil_matrix((self.n_gen_disp_sh, self.NV - 2 * self.nbus - 2 * self.n_gen_disp_sh))])
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
Hvu = sp.hstack([lil_matrix((self.npq, self.nbus)), diags_bus_ones[self.pq, :],
lil_matrix((self.npq, 2 * self.n_gen_disp_sh + 2 * self.n_br_mon)),
- diags_pq_ones, lil_matrix(
(self.npq, self.npq + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
Hvl = sp.hstack(
[lil_matrix((self.npq, self.nbus)), -diags_bus_ones[self.pq, :],
lil_matrix((self.npq, 2 * self.n_gen_disp_sh + 2 * self.n_br_mon + self.npq)),
- diags_pq_ones,
lil_matrix((self.npq, self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
Hslsf = sp.hstack([lil_matrix((self.n_br_mon, npfvar)), - diags_br_mon_ones,
lil_matrix((self.n_br_mon,
self.n_br_mon + 2 * self.npq + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
Hslst = sp.hstack([lil_matrix((self.n_br_mon, npfvar + self.n_br_mon)), - diags_br_mon_ones,
lil_matrix((self.n_br_mon,
2 * self.npq + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
Hslvmax = sp.hstack([lil_matrix((self.npq, npfvar + 2 * self.n_br_mon)), - diags_pq_ones,
lil_matrix(
(self.npq,
self.npq + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
Hslvmin = sp.hstack(
[lil_matrix((self.npq, npfvar + 2 * self.n_br_mon + self.npq)), - diags_pq_ones,
lil_matrix((self.npq, self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
else:
Hvu = sp.hstack([lil_matrix((self.npq, self.nbus)), diags_pq_ones,
lil_matrix((self.npq, self.NV - self.nbus - self.npq))])
Hvl = sp.hstack([lil_matrix((self.npq, self.nbus)), - diags_pq_ones,
lil_matrix((self.npq, self.NV - self.nbus - self.npq))])
Hslsf = lil_matrix((0, self.NV))
Hslst = lil_matrix((0, self.NV))
Hslvmax = lil_matrix((0, self.NV))
Hslvmin = lil_matrix((0, self.NV))
if (self.ntapm + self.ntapt) != 0:
Sftapm = dSfdm[self.br_mon_idx, :].copy()
Sftapt = dSfdt[self.br_mon_idx, :].copy()
Sttapm = dStdm[self.br_mon_idx, :].copy()
Sttapt = dStdt[self.br_mon_idx, :].copy()
SfX = sp.hstack(
[Sfva, Sfvm, lil_matrix((self.n_br_mon, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)), Sftapm,
Sftapt,
lil_matrix((self.n_br_mon, self.n_disp_hvdc))])
StX = sp.hstack(
[Stva, Stvm, lil_matrix((self.n_br_mon, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)), Sttapm,
Sttapt,
lil_matrix((self.n_br_mon, self.n_disp_hvdc))])
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
# Equivalent to HSf = 2 * (Sfmat.real @ SfX.real + Sfmat.imag @ SfX.imag) + Hslsf
HSfdata = self.Sf.real[SfX.row] * SfX.data.real + self.Sf.imag[SfX.row] * SfX.data.imag
HSf = 2 * csc((HSfdata, (SfX.row, SfX.col)), shape=(self.n_br_mon, self.NV)) + Hslsf
HStdata = self.St.real[SfX.row] * StX.data.real + self.St.imag[StX.row] * StX.data.imag
HSt = 2 * csc((HStdata, (StX.row, StX.col)), shape=(self.n_br_mon, self.NV)) + Hslst
else:
# Equivalent to HSf = 2 * (Sfmat.real @ SfX.real + Sfmat.imag @ SfX.imag)
HSfdata = self.Sf.real[SfX.row] * SfX.data.real + self.Sf.imag[SfX.row] * SfX.data.imag
HSf = 2 * csc((HSfdata, (SfX.row, SfX.col)), shape=(self.n_br_mon, self.NV))
HStdata = self.St.real[StX.row] * StX.data.real + self.St.imag[StX.row] * StX.data.imag
HSt = 2 * csc((HStdata, (StX.row, StX.col)), shape=(self.n_br_mon, self.NV))
if self.ntapm != 0:
Htapmu = sp.hstack(
[lil_matrix((self.ntapm, npfvar + self.nsl + self.nslcap)), diags(np.ones(self.ntapm)),
lil_matrix((self.ntapm, self.ntapt + self.n_disp_hvdc + nvc))])
Htapml = sp.hstack(
[lil_matrix((self.ntapm, npfvar + self.nsl + self.nslcap)), diags(- np.ones(self.ntapm)),
lil_matrix((self.ntapm, self.ntapt + self.n_disp_hvdc + nvc))])
else:
Htapmu = lil_matrix((0, self.NV))
Htapml = lil_matrix((0, self.NV))
if self.ntapt != 0:
Htaptu = sp.hstack(
[lil_matrix((self.ntapt, npfvar + self.nsl + self.nslcap + self.ntapm)),
diags(np.ones(self.ntapt)),
lil_matrix((self.ntapt, self.n_disp_hvdc + nvc))])
Htaptl = sp.hstack([lil_matrix((self.ntapt, npfvar + self.nsl + self.nslcap + self.ntapm)),
diags(- np.ones(self.ntapt)),
lil_matrix((self.ntapt, self.n_disp_hvdc + nvc))])
else:
Htaptu = lil_matrix((0, self.NV))
Htaptl = lil_matrix((0, self.NV))
else:
Sftapm = lil_matrix((self.n_br_mon, self.ntapm))
Sttapm = lil_matrix((self.n_br_mon, self.ntapm))
Sftapt = lil_matrix((self.n_br_mon, self.ntapt))
Sttapt = lil_matrix((self.n_br_mon, self.ntapt))
Htapmu = lil_matrix((self.ntapm, self.NV))
Htapml = lil_matrix((self.ntapm, self.NV))
Htaptu = lil_matrix((self.ntapt, self.NV))
Htaptl = lil_matrix((self.ntapt, self.NV))
SfX = sp.hstack(
[Sfva, Sfvm,
lil_matrix((self.n_br_mon, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc))])
StX = sp.hstack(
[Stva, Stvm,
lil_matrix((self.n_br_mon, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc))])
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
# HSf = 2 * (Sfmat.real @ SfX.real + Sfmat.imag @ SfX.imag) + Hslsf
# HSt = 2 * (Stmat.real @ StX.real + Stmat.imag @ StX.imag)
HSfdata = self.Sf.real[SfX.row] * SfX.data.real + self.Sf.imag[SfX.row] * SfX.data.imag
HSf = 2 * csc((HSfdata, (SfX.row, SfX.col)), shape=(self.n_br_mon, self.NV)) + Hslsf
HStdata = self.St.real[StX.row] * StX.data.real + self.St.imag[StX.row] * StX.data.imag
HSt = 2 * csc((HStdata, (StX.row, StX.col)), shape=(self.n_br_mon, self.NV)) + Hslst
else:
# HSf = 2 * (Sfmat.real @ SfX.real + Sfmat.imag @ SfX.imag)
# HSt = 2 * (Stmat.real @ StX.real + Stmat.imag @ StX.imag)
HSfdata = self.Sf.real[SfX.row] * SfX.data.real + self.Sf.imag[SfX.row] * SfX.data.imag
HSf = 2 * csc((HSfdata, (SfX.row, SfX.col)), shape=(self.n_br_mon, self.NV))
HStdata = self.St.real[StX.row] * StX.data.real + self.St.imag[StX.row] * StX.data.imag
HSt = 2 * csc((HStdata, (StX.row, StX.col)), shape=(self.n_br_mon, self.NV))
if self.options.ips_control_q_limits: # if reactive power control...
# tanmax curves (simplified capability curves of generators)
Hqmaxp = 2 * self.Pg[:self.n_gen_disp]
Hqmaxq = 2 * self.Qg[:self.n_gen_disp]
# Hqmaxv = - 2 * diags(np.power(self.Inom, 2.0)) * self.Cdispgen_t @ diags(
# self.Vm)
Hqmaxv_data = np.power(self.Inom, 2) * self.Vm[self.gen_bus_idx[self.gen_disp_idx]]
Hqmaxv = csc((- 2 * Hqmaxv_data, (np.arange(self.n_gen_disp), self.gen_bus_idx[self.gen_disp_idx])),
shape=(self.n_gen_disp, self.nbus))
Hqmax = sp.hstack(
[lil_matrix((nqct, self.nbus)), Hqmaxv, diags(Hqmaxp), lil_matrix((nqct, self.nsh)), diags(Hqmaxq),
lil_matrix((nqct, self.nsh)),
lil_matrix((nqct, self.nsl + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
else:
Hqmax = lil_matrix((nqct, self.NV))
Hdcu = sp.hstack([lil_matrix((self.n_disp_hvdc, self.NV - self.n_disp_hvdc - nvc)),
diags_disp_hvdc_ones, lil_matrix((self.n_disp_hvdc, nvc))])
Hdcl = sp.hstack([lil_matrix((self.n_disp_hvdc, self.NV - self.n_disp_hvdc - nvc)),
- diags_disp_hvdc_ones, lil_matrix((self.n_disp_hvdc, nvc))])
# Nodal capacity sign constraint Jacobian: d/dx(sign * slcap) = sign * I at slcap positions
if self.nslcap != 0:
Hslcap = sp.hstack([lil_matrix((self.nslcap, npfvar + self.nsl)),
diags(np.sign(self.nodal_capacity_sign) * np.ones(self.nslcap)),
lil_matrix((self.nslcap, self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
else:
Hslcap = lil_matrix((0, self.NV))
# VSC inequality rows (same role as Hdcu/Hdcl for the HVDC links):
# -It <= 0 ; It - rate/Sbase (- sl_vsc) <= 0 ; -sl_vsc <= 0
Hvscit = csc_matrix((-np.ones(self.nvsc), (_vsc_k, col_It)),
shape=(self.nvsc, self.NV))
if self.n_vsc_lim:
_j = np.arange(self.n_vsc_lim)
cur_r = list(_j)
cur_c = list(col_It[self.vsc_lim_idx])
cur_v = [1.0] * self.n_vsc_lim
if self.n_sl_vsc:
cur_r += list(_j)
cur_c += list(col_sl_vsc)
cur_v += [-1.0] * self.n_vsc_lim
Hvsccur = csc_matrix((cur_v, (cur_r, cur_c)), shape=(self.n_vsc_lim, self.NV))
else:
Hvsccur = csc((0, self.NV))
if self.n_sl_vsc:
_j = np.arange(self.n_sl_vsc)
Hvscsl = csc_matrix((-np.ones(self.n_sl_vsc), (_j, col_sl_vsc)),
shape=(self.n_sl_vsc, self.NV))
else:
Hvscsl = csc((0, self.NV))
Hx = sp.vstack([
HSf,
HSt,
Hvu,
Hpu,
Hqu,
Hvl,
Hpl,
Hql,
Hslsf,
Hslst,
Hslvmax,
Hslvmin,
Htapmu,
Htapml,
Htaptu,
Htaptl,
Hqmax,
Hdcu,
Hdcl,
Hslcap,
Hvscit,
Hvsccur,
Hvscsl,
])
Hx = Hx.tocsc()
te_hx = timeit.default_timer()
# HESSIANS ---------------------------------------------------------------------------------------------------------
if compute_hessians:
# OBJECTIVE FUNCTION HESS --------------------------------------------------------------------------------------
ts_fxx = timeit.default_timer()
if self.nslcap == 0:
fxx = diags((np.r_[
np.zeros(2 * self.nbus),
2 * self.c2 * (self.Sbase * self.Sbase),
np.zeros(self.n_gen_disp_sh + self.nsl + self.nslcap + self.ntapm + self.ntapt
+ self.n_disp_hvdc + nvc)
]) * 1e-4).tocsc()
else:
fxx = csc((self.NV, self.NV))
te_fxx = timeit.default_timer()
# EQUALITY CONSTRAINTS HESS ------------------------------------------------------------------------------------
'''
The following matrix represents the structure of the hessian matrix for the equality constraints
N N Ng Ng nsl ntapm ntapt ndc vsc
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
N | Gvava | Gvavm | Gvapg | Gvaqg | Gvasl | Gvatapm | Gvatapt | Gvapdc | Gvavsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
N | Gvmva | Gvmvm | Gvmpg | Gvmqg | Gvmsl | Gvmtapm | Gvmtapt | Gvmpdc | Gvmvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
Ng | Gpgva | Gpgvm | Gpgpg | Gpgqg | Gpgsl | Gpgtapm | Gpgtapt | Gpgpdc | Gpgvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
Ng | Gqgva | Gqgvm | Gqgpg | Gqgqg | Gqgsl | Gqgtapm | Gqgtapt | Gqgpdc | Gqgvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
nsl | Gslva | Gslvm | Gslpg | Gslqg | Gslsl | Gsltapm | Gsltapt | Gslpdc | Gslvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ntapm | Gtapmva | Gtapmvm | Gtapmpg | Gtapmqg | Gtapmsl | Gtapmtapm | Gtapmtapt | Gtapmpdc | Gtapmvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ntapt | Gtaptva | Gtaptvm | Gtaptpg | Gtaptqg | Gtaptsl | Gtapttapm | Gtapttapt | Gtaptpdc | Gtaptvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ndc | Gpdcva | Gpdcvm | Gpdcpg | Gpdcqg | Gpdcsl | Gpdctapm | Gpdctapt | Gpdcpdc | Gpdcvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
vsc | Gvscva | Gvscvm | Gvscpg | Gvscqg | Gvscsl | Gvsctapm | Gvsctapt | Gvscpdc | Gvscvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
The VSC extension adds a trailing variable group
vsc = [Pt | Qt | Pf | It | slV] (one extra row/col band), so Gxx is
full (NV x NV). The Pfdc/AC blocks above are unchanged. The only
non-zero VSC second derivatives come from the two new equality rows
(weighted by their multipliers lam_loss, lam_curd):
loss : d2/dIt2 = 2*a3
curdef : d2/dIt2 = 2*Vm[T]^2 ; d2/dVm[T]2 = 2*It^2 ;
d2/dIt dVm[T] = 4*It*Vm[T] ; d2/dPt2 = d2/dQt2 = -2
'''
ts_gxx = timeit.default_timer()
# P
lam_p = lam[0: self.nbus]
lam_diag_p = diags(lam_p)
B_p = np.conj(self.admittances.Ybus @ Vmat)
# data = self.admittances.Ybus.data * self.V[self.Ybus_indices]
# B_p = csc_matrix((np.conj(data), self.Ybus_indices, self.Ybus_indptr),
# shape=(self.nbus, self.nbus)).transpose()
D_p = np.conj(self.admittances.Ybus).T @ Vmat
# data = np.conj(self.admittances.Ybus.data) * self.V[self.Ybus_cols]
# D_p = csc_matrix((data, (self.Ybus_cols, self.Ybus_indices)),
# shape=(self.nbus, self.nbus)).transpose()
I_p = np.conj(Vmat) @ (D_p @ lam_diag_p - diags(D_p @ lam_p))
F_p = lam_diag_p @ Vmat @ (B_p - diags(np.conj(Ibus)))
C_p = lam_diag_p @ Vmat @ B_p
Gaa_p = I_p + F_p
Gva_p = 1j * vm_inv @ (I_p - F_p)
Gvv_p = vm_inv @ (C_p + C_p.T) @ vm_inv
# Q
lam_q = lam[self.nbus: 2 * self.nbus]
lam_diag_q = diags(lam_q)
B_q = np.conj(self.admittances.Ybus @ Vmat)
D_q = np.conj(self.admittances.Ybus).T @ Vmat
I_q = np.conj(Vmat) @ (D_q @ lam_diag_q - diags(D_q @ lam_q))
F_q = lam_diag_q @ Vmat @ (B_q - diags(np.conj(Ibus)))
C_q = lam_diag_q @ Vmat @ B_q
Gaa_q = I_q + F_q
Gva_q = 1j * vm_inv @ (I_q - F_q)
Gvv_q = vm_inv @ (C_q + C_q.T) @ vm_inv
Gaa = Gaa_p.real + Gaa_q.imag
Gva = Gva_p.real + Gva_q.imag
Gav = Gva.T
Gvv = Gvv_p.real + Gvv_q.imag
(GSdmdm, dSfdmdm, dStdmdm,
GSdmdvm, dSfdmdvm, dStdmdvm,
GSdmdva, dSfdmdva, dStdmdva,
GSdmdt, dSfdmdt, dStdmdt,
GSdtdt, dSfdtdt, dStdtdt,
GSdtdvm, dSfdtdvm, dStdtdvm,
GSdtdva, dSfdtdva, dStdtdva) = self.compute_branch_power_second_derivatives(lam[0: 2 * self.nbus],
mu[0: 2 * self.n_br_mon])
if self.ntapm + self.ntapt != 0:
G1 = sp.hstack(
[Gaa, Gav, lil_matrix((self.nbus, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)), GSdmdva,
GSdtdva,
lil_matrix((self.nbus, self.n_disp_hvdc + nvc))])
G2 = sp.hstack(
[Gva, Gvv, lil_matrix((self.nbus, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)), GSdmdvm,
GSdtdvm,
lil_matrix((self.nbus, self.n_disp_hvdc + nvc))])
G3 = sp.hstack(
[GSdmdva.T, GSdmdvm.T, lil_matrix((self.ntapm, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
GSdmdm, GSdmdt.T, lil_matrix((self.ntapm, self.n_disp_hvdc + nvc))])
G4 = sp.hstack(
[GSdtdva.T, GSdtdvm.T, lil_matrix((self.ntapt, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
GSdmdt, GSdtdt, lil_matrix((self.ntapt, self.n_disp_hvdc + nvc))])
Gxx = sp.vstack(
[G1, G2, lil_matrix((2 * self.n_gen_disp_sh + self.nsl + self.nslcap, self.NV)), G3, G4,
lil_matrix((self.n_disp_hvdc + nvc, self.NV))]).tocsc()
else:
G1 = sp.hstack(
[Gaa, Gav,
lil_matrix((self.nbus,
2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
G2 = sp.hstack(
[Gva, Gvv,
lil_matrix((self.nbus,
2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
Gxx = sp.vstack(
[G1, G2, lil_matrix((2 * self.n_gen_disp_sh + self.nsl + self.nslcap
+ self.n_disp_hvdc + nvc, self.NV))]).tocsc()
# VSC equality-constraint second derivatives (loss + Ilim rows),
# weighted by their multipliers. -It<=0 / It-rate<=0 are linear.
if self.nvsc:
base = 2 * self.nbus + self.n_slack + self.npv
lam_loss = lam[base: base + self.nvsc]
lam_curd = lam[base + self.nvsc: base + 2 * self.nvsc]
col_Vm_t = self.nbus + self.T_vsc
hr = np.concatenate([col_It,
col_It, col_Vm_t, col_It, col_Vm_t,
col_Pt, col_Qt])
hc = np.concatenate([col_It,
col_It, col_Vm_t, col_Vm_t, col_It,
col_Pt, col_Qt])
hv = np.concatenate([lam_loss * 2.0 * self.vsc_alpha3,
lam_curd * 2.0 * vm_t ** 2,
lam_curd * 2.0 * self.It_vsc ** 2,
lam_curd * 4.0 * self.It_vsc * vm_t,
lam_curd * 4.0 * self.It_vsc * vm_t,
lam_curd * (-2.0) * np.ones(self.nvsc),
lam_curd * (-2.0) * np.ones(self.nvsc)])
Gxx = (Gxx + csc_matrix((hv, (hr, hc)), shape=(self.NV, self.NV))).tocsc()
te_gxx = timeit.default_timer()
# INEQUALITY CONSTRAINTS HESS ----------------------------------------------------------------------------------
'''
The following matrix represents the structure of the hessian matrix for the inequality constraints
N N Ng Ng nsl ntapm ntapt ndc vsc
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
N | Hvava | Hvavm | Hvapg | Hvaqg | Hvasl | Hvatapm | Hvatapt | Hvapdc | Hvavsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
N | Hvmva | Hvmvm | Hvmpg | Hvmqg | Hvmsl | Hvmtapm | Hvmtapt | Hvmpdc | Hvmvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
Ng | Hpgva | Hpgvm | Hpgpg | Hpgqg | Hpgsl | Hpgtapm | Hpgtapt | Hpgpdc | Hpgvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
Ng | Hqgva | Hqgvm | Hqgpg | Hqgqg | Hqgsl | Hqgtapm | Hqgtapt | Hqgpdc | Hqgvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
nsl | Hslva | Hslvm | Hslpg | Hslqg | Hslsl | Hsltapm | Hsltapt | Hslpdc | Hslvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ntapm | Htapmva | Htapmvm | Htapmpg | Htapmqg | Htapmsl | Htapmtapm | Htapmtapt | Htapmpdc | Htapmvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ntapt | Htaptva | Htaptvm | Htaptpg | Htaptqg | Htaptsl | Htapttapm | Htapttapt | Htaptpdc | Htaptvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
ndc | Hpdcva | Hpdcvm | Hpdcpg | Hpdcqg | Hpdcsl | Hpdctapm | Hpdctapt | Hpdcpdc | Hpdcvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
vsc | Hvscva | Hvscvm | Hvscpg | Hvscqg | Hvscsl | Hvsctapm | Hvsctapt | Hvscpdc | Hvscvsc |
+---------+---------+---------+---------+---------+-----------+-----------+----------+----------+
Hxx is padded to the full (NV x NV) including the trailing
vsc = [Pt | Qt | Pf | It | slV] band. The three VSC inequalities
(-It <= 0, It - rate/Sb (- slV) <= 0, -slV <= 0) are all linear, so
their second derivative is zero
'''
ts_hxx = timeit.default_timer()
muf = mu[0: self.n_br_mon]
mut = mu[self.n_br_mon: 2 * self.n_br_mon]
muf_mat = diags(muf)
mut_mat = diags(mut)
Smuf_mat = diags(Sfmat.conj() @ muf)
Smut_mat = diags(Stmat.conj() @ mut)
Af = np.conj(Yf_mon).T @ Smuf_mat @ self.Cfmon
Bf = np.conj(Vmat) @ Af @ Vmat
Df = diags(Af @ self.V) @ np.conj(Vmat)
Ef = diags(Af.T @ np.conj(self.V)) @ Vmat
Ff = Bf + Bf.T
Sfvava = Ff - Df - Ef
Sfvmva = 1j * vm_inv @ (Bf - Bf.T - Df + Ef)
Sfvavm = Sfvmva.T
Sfvmvm = vm_inv @ Ff @ vm_inv
if self.options.ips_control_q_limits: # using reactive power control
# TODO: Avoid negative slicing
b = None # This allows proper slicing of the mu vector when n_disp_hvdc is 0.
if self.n_disp_hvdc != 0:
b = - 2 * self.n_disp_hvdc
Hqpgpg = diags(
np.r_[np.array([2] * self.n_gen_disp) * mu[- self.n_gen_disp - 2 * self.n_disp_hvdc:b],
np.zeros(self.nsh)]
)
Hqqgqg = diags(
np.r_[np.array([2] * self.n_gen_disp) * mu[- self.n_gen_disp - 2 * self.n_disp_hvdc:b],
np.zeros(self.nsh)]
)
Hqvmvm = (self.Cdispgen @ diags(mu[- self.n_gen_disp - 2 * self.n_disp_hvdc: b])
[docs]
@ (- 2 * diags(np.power(self.Inom, 2.0)) * self.Cdispgen_t))
else:
Hqpgpg = lil_matrix((self.n_gen_disp_sh, self.n_gen_disp_sh))
Hqqgqg = lil_matrix((self.n_gen_disp_sh, self.n_gen_disp_sh))
Hqvmvm = lil_matrix((self.nbus, self.nbus))
Hfvava = 2 * (Sfvava + Sfva.T @ muf_mat @ np.conj(Sfva)).real
Hfvmva = 2 * (Sfvmva + Sfvm.T @ muf_mat @ np.conj(Sfva)).real
Hfvavm = 2 * (Sfvavm + Sfva.T @ muf_mat @ np.conj(Sfvm)).real
Hfvmvm = 2 * (Sfvmvm + Sfvm.T @ muf_mat @ np.conj(Sfvm)).real
At = np.conj(Yt_mon).T @ Smut_mat @ self.Ctmon
Bt = np.conj(Vmat) @ At @ Vmat
Dt = diags(At @ self.V) @ np.conj(Vmat)
Et = diags(At.T @ np.conj(self.V)) @ Vmat
Ft = Bt + Bt.T
Stvava = Ft - Dt - Et
Stvmva = 1j * vm_inv @ (Bt - Bt.T - Dt + Et)
Stvavm = Stvmva.T
Stvmvm = vm_inv @ Ft @ vm_inv
Htvava = 2 * (Stvava + Stva.T @ mut_mat @ np.conj(Stva)).real
Htvmva = 2 * (Stvmva + Stvm.T @ mut_mat @ np.conj(Stva)).real
Htvavm = 2 * (Stvavm + Stva.T @ mut_mat @ np.conj(Stvm)).real
Htvmvm = 2 * (Stvmvm + Stvm.T @ mut_mat @ np.conj(Stvm)).real
if self.ntapm + self.ntapt != 0:
Hftapmva = 2 * (dSfdmdva.T + Sftapm.T @ muf_mat @ np.conj(Sfva)).real
Hftapmvm = 2 * (dSfdmdvm.T + Sftapm.T @ muf_mat @ np.conj(Sfvm)).real
Hftaptva = 2 * (dSfdtdva.T + Sftapt.T @ muf_mat @ np.conj(Sfva)).real
Hftaptvm = 2 * (dSfdtdvm.T + Sftapt.T @ muf_mat @ np.conj(Sfvm)).real
Hftapmtapm = 2 * (dSfdmdm.T + Sftapm.T @ muf_mat @ np.conj(Sftapm)).real
Hftapttapt = 2 * (dSfdtdt.T + Sftapt.T @ muf_mat @ np.conj(Sftapt)).real
Hftapmtapt = 2 * (dSfdmdt.T + Sftapm.T @ muf_mat @ np.conj(Sftapt)).real
Httapmva = 2 * (dStdmdva.T + Sttapm.T @ mut_mat @ np.conj(Stva)).real
Httapmvm = 2 * (dStdmdvm.T + Sttapm.T @ mut_mat @ np.conj(Stvm)).real
Httaptva = 2 * (dStdtdva.T + Sttapt.T @ mut_mat @ np.conj(Stva)).real
Httaptvm = 2 * (dStdtdvm.T + Sttapt.T @ mut_mat @ np.conj(Stvm)).real
Httapmtapm = 2 * (dStdmdm.T + Sttapm.T @ mut_mat @ np.conj(Sttapm)).real
Httapttapt = 2 * (dStdtdt.T + Sttapt.T @ mut_mat @ np.conj(Sttapt)).real
Httapmtapt = 2 * (dStdmdt.T + Sttapm.T @ mut_mat @ np.conj(Sttapt)).real
H1 = sp.hstack([Hfvava + Htvava,
Hfvavm + Htvavm,
lil_matrix((self.nbus, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
Hftapmva.T + Httapmva.T,
Hftaptva.T + Httaptva.T,
lil_matrix((self.nbus, self.n_disp_hvdc + nvc))])
H2 = sp.hstack([Hfvmva + Htvmva,
Hfvmvm + Htvmvm + Hqvmvm,
lil_matrix((self.nbus, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
Hftapmvm.T + Httapmvm.T,
Hftaptvm.T + Httaptvm.T,
lil_matrix((self.nbus, self.n_disp_hvdc + nvc))])
H3 = sp.hstack(
[lil_matrix((self.n_gen_disp_sh, 2 * self.nbus)), Hqpgpg, lil_matrix(
(self.n_gen_disp_sh,
self.n_gen_disp_sh + self.nsl + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
H4 = sp.hstack(
[lil_matrix((self.n_gen_disp_sh,
2 * self.nbus + self.n_gen_disp_sh)),
Hqqgqg,
lil_matrix((self.n_gen_disp_sh,
self.nsl + self.nslcap + self.ntapm + self.ntapt + self.n_disp_hvdc + nvc))])
H5 = sp.hstack([Hftapmva + Httapmva,
Hftapmvm + Httapmvm,
lil_matrix((self.ntapm, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
Hftapmtapm + Httapmtapm,
Hftapmtapt + Httapmtapt,
lil_matrix((self.ntapm, self.n_disp_hvdc + nvc))])
H6 = sp.hstack([Hftaptva + Httaptva,
Hftaptvm + Httaptvm,
lil_matrix((self.ntapt, 2 * self.n_gen_disp_sh + self.nsl + self.nslcap)),
Hftapmtapt.T + Httapmtapt.T,
Hftapttapt + Httapttapt,
lil_matrix((self.ntapt, self.n_disp_hvdc + nvc))])
Hxx = sp.vstack([H1, H2, H3, H4, lil_matrix((self.nsl + self.nslcap, self.NV)), H5, H6,
lil_matrix((self.n_disp_hvdc + nvc, self.NV))]).tocsc()
else:
H1 = sp.hstack([Hfvava + Htvava,
Hfvavm + Htvavm,
lil_matrix((self.nbus,
2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
H2 = sp.hstack([Hfvmva + Htvmva,
Hfvmvm + Htvmvm + Hqvmvm,
lil_matrix((self.nbus,
2 * self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
H3 = sp.hstack([lil_matrix((self.n_gen_disp_sh, 2 * self.nbus)),
Hqpgpg,
lil_matrix((self.n_gen_disp_sh,
self.n_gen_disp_sh + self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
H4 = sp.hstack([lil_matrix((self.n_gen_disp_sh, 2 * self.nbus + self.n_gen_disp_sh)), Hqqgqg,
lil_matrix((self.n_gen_disp_sh, self.nsl + self.nslcap + self.n_disp_hvdc + nvc))])
Hxx = sp.vstack([H1, H2, H3, H4,
lil_matrix((self.nsl + self.nslcap + self.n_disp_hvdc + nvc, self.NV))]).tocsc()
te_hxx = timeit.default_timer()
else:
# Return empty structures
fxx = csc((self.NV, self.NV))
Gxx = csc((self.NV, self.NV))
Hxx = csc((self.NV, self.NV))
ts_fxx = 0
te_fxx = 0
ts_gxx = 0
te_gxx = 0
ts_hxx = 0
te_hxx = 0
# # Save to csv fx, Gx, Hx, fxx, Gxx, Hxx
#
# fx.tofile('fx_n.csv', sep=',')
# Gx.toarray().tofile('Gx_n.csv', sep=',')
# Hx.toarray().tofile('Hx_n.csv', sep=',')
# fxx.toarray().tofile('fxx_n.csv', sep=',')
# Gxx.toarray().tofile('Gxx_n.csv', sep=',')
# Hxx.toarray().tofile('Hxx_n.csv', sep=',')
#
# print(1000 * np.array([te_fx - ts_fx,
# te_gx - ts_gx,
# te_hx - ts_hx,
# te_fxx - ts_fxx,
# te_gxx - ts_gxx,
# te_hxx - ts_hxx]), 100 * np.array([te_fx - ts_fx,
# te_gx - ts_gx,
# te_hx - ts_hx,
# te_fxx - ts_fxx,
# te_gxx - ts_gxx,
# te_hxx - ts_hxx]) / (te_hxx - ts_fx))
return fx, Gx, Hx, fxx, Gxx, Hxx
def compute_branch_power_derivatives(self) -> Tuple[
csr_matrix, lil_matrix, lil_matrix, csr_matrix, lil_matrix, lil_matrix]:
"""
TODO: Move outside of the class
:return: First power derivatives with respect to the tap variables
[dSbusdm, dSfdm, dStdm, dSbusdt, dSfdtau, dStdtau]
"""
ys = 1.0 / (self.R + 1.0j * self.X + 1e-20)
nbr = len(self.all_tap_m)
dSfdm = lil_matrix((nbr, self.ntapm), dtype=complex)
dStdm = lil_matrix((nbr, self.ntapm), dtype=complex)
dSfdtau = lil_matrix((nbr, self.ntapt), dtype=complex)
dStdtau = lil_matrix((nbr, self.ntapt), dtype=complex)
for k_pos, k in enumerate(self.k_m):
Vf = self.V[self.from_idx[k]]
Vt = self.V[self.to_idx[k]]
mp = self.all_tap_m[k]
tau = self.all_tap_tau[k]
yk = ys[k]
mp2 = np.power(mp, 2)
# First derivatives with respect to the tap module.
# Each branch is computed individually and stored
dSfdm[k, k_pos] = Vf * (
(-2 * np.conj(yk * Vf) / np.power(mp, 3)) + np.conj(yk * Vt) / (mp2 * np.exp(1j * tau)))
dStdm[k, k_pos] = Vt * (np.conj(yk * Vf) / (mp2 * np.exp(-1j * tau)))
for k_pos, k in enumerate(self.k_tau):
Vf = self.V[self.from_idx[k]]
Vt = self.V[self.to_idx[k]]
mp = self.all_tap_m[k]
tau = self.all_tap_tau[k]
yk = ys[k]
# First derivatives with respect to the tap phase.
# Each branch is computed individually and stored
dSfdtau[k, k_pos] = Vf * 1j * np.conj(yk * Vt) / (mp * np.exp(1j * tau))
dStdtau[k, k_pos] = Vt * -1j * np.conj(yk * Vf) / (mp * np.exp(-1j * tau))
# Bus power injection is computed using the 'from' and 'to' powers and their connectivity matrices
dSbusdm = self.admittances.Cf.T @ dSfdm + self.admittances.Ct.T @ dStdm
dSbusdt = self.admittances.Cf.T @ dSfdtau + self.admittances.Ct.T @ dStdtau
return dSbusdm, dSfdm, dStdm, dSbusdt, dSfdtau, dStdtau
[docs]
def compute_branch_power_second_derivatives(self, lam: Vec, mu: Vec) -> Tuple[
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix,
lil_matrix, lil_matrix, lil_matrix]:
"""
TODO: Move outside of the class
:param lam: Lambda multiplier
:param mu: Mu multiplier
:return: Power second derivatives with respect to tap variables
"""
ys = 1.0 / (self.R + 1.0j * self.X + 1e-20)
dSbusdmdm = lil_matrix((self.ntapm, self.ntapm))
dSfdmdm = lil_matrix((self.ntapm, self.ntapm), dtype=complex)
dStdmdm = lil_matrix((self.ntapm, self.ntapm), dtype=complex)
dSbusdmdva = lil_matrix((self.nbus, self.ntapm))
dSfdmdva = lil_matrix((self.nbus, self.ntapm), dtype=complex)
dStdmdva = lil_matrix((self.nbus, self.ntapm), dtype=complex)
dSbusdmdvm = lil_matrix((self.nbus, self.ntapm))
dSfdmdvm = lil_matrix((self.nbus, self.ntapm), dtype=complex)
dStdmdvm = lil_matrix((self.nbus, self.ntapm), dtype=complex)
dSbusdtdt = lil_matrix((self.ntapt, self.ntapt))
dSfdtdt = lil_matrix((self.ntapt, self.ntapt), dtype=complex)
dStdtdt = lil_matrix((self.ntapt, self.ntapt), dtype=complex)
dSbusdtdva = lil_matrix((self.nbus, self.ntapt))
dSfdtdva = lil_matrix((self.nbus, self.ntapt), dtype=complex)
dStdtdva = lil_matrix((self.nbus, self.ntapt), dtype=complex)
dSbusdtdvm = lil_matrix((self.nbus, self.ntapt))
dSfdtdvm = lil_matrix((self.nbus, self.ntapt), dtype=complex)
dStdtdvm = lil_matrix((self.nbus, self.ntapt), dtype=complex)
dSbusdmdt = lil_matrix((self.ntapt, self.ntapm))
dSfdmdt = lil_matrix((self.ntapt, self.ntapm), dtype=complex)
dStdmdt = lil_matrix((self.ntapt, self.ntapm), dtype=complex)
for k_pos, k in enumerate(self.k_m):
f = self.from_idx[k]
t = self.to_idx[k]
Vf = self.V[f]
Vt = self.V[t]
mp = self.all_tap_m[k]
tau = self.all_tap_tau[k]
yk = ys[k]
tap_unit = np.exp(1j * tau)
tap_unit_c = np.exp(-1j * tau)
# For each line with a module controlled transformer, compute its second derivatives w.r.t. the tap module and
# the rest of the variables.
mp2 = mp * mp
mp3 = mp2 * mp
mp4 = mp3 * mp
dSfdmdm_ = Vf * ((6 * np.conj(yk * Vf) / mp4) - 2 * np.conj(yk * Vt) / (mp3 * tap_unit))
dStdmdm_ = - Vt * 2 * np.conj(yk * Vf) / (mp3 * tap_unit_c)
dSfdmdva_f = Vf * 1j * np.conj(yk * Vt) / (mp2 * tap_unit)
dSfdmdva_t = - Vf * 1j * np.conj(yk * Vt) / (mp2 * tap_unit)
dStdmdva_f = - Vt * 1j * np.conj(yk * Vf) / (mp2 * tap_unit_c)
dStdmdva_t = Vt * 1j * np.conj(yk * Vf) / (mp2 * tap_unit_c)
dSfdmdvm_f = Vf * (1 / self.Vm[f]) * ((-4 * np.conj(yk * Vf) / mp3) + np.conj(yk * Vt) / (mp2 * tap_unit))
dSfdmdvm_t = Vf * (1 / self.Vm[t]) * np.conj(yk * Vt) / (mp2 * tap_unit)
dStdmdvm_f = Vt * (1 / self.Vm[f]) * np.conj(yk * Vf) / (mp2 * tap_unit_c)
dStdmdvm_t = Vt * (1 / self.Vm[t]) * np.conj(yk * Vf) / (mp2 * tap_unit_c)
lin = np.where(self.k_tau == k)[0] # TODO: should pass along the control type and check that instead
if len(lin) != 0:
k_pos = lin[0]
# If the trafo is controlled for both module and phase, compute these derivatives. Otherwise, they are 0
dSfdmdt_ = - Vf * 1j * (np.conj(yk * Vt) / (mp2 * tap_unit))
dStdmdt_ = Vt * 1j * (np.conj(yk * Vf) / (mp2 * tap_unit_c))
dSbusdmdt[k_pos, k_pos] = ((dSfdmdt_ * lam[f]).real + (dSfdmdt_ * lam[f + self.nbus]).imag
+ (dStdmdt_ * lam[t]).real + (dStdmdt_ * lam[t + self.nbus]).imag)
if k in self.br_mon_idx:
# This is only included if the branch is monitored.
li = np.where(self.br_mon_idx == k)[0] # TODO: Why is this here?
dSfdmdt[k_pos, k_pos] = dSfdmdt_ * self.Sf[li].conj() * mu[li]
dStdmdt[k_pos, k_pos] = dStdmdt_ * self.St[li].conj() * mu[li + self.n_br_mon]
# Compute the hessian terms merging Sf and St into Sbus
dSbusdmdm[k_pos, k_pos] = ((dSfdmdm_ * lam[f]).real + (dSfdmdm_ * lam[f + self.nbus]).imag
+ (dStdmdm_ * lam[t]).real + (dStdmdm_ * lam[t + self.nbus]).imag)
dSbusdmdva[f, k_pos] = ((dSfdmdva_f * lam[f]).real + (dSfdmdva_f * lam[f + self.nbus]).imag
+ (dStdmdva_f * lam[t]).real + (dStdmdva_f * lam[t + self.nbus]).imag)
dSbusdmdva[t, k_pos] = ((dSfdmdva_t * lam[f]).real + (dSfdmdva_t * lam[f + self.nbus]).imag
+ (dStdmdva_t * lam[t]).real + (dStdmdva_t * lam[t + self.nbus]).imag)
dSbusdmdvm[f, k_pos] = ((dSfdmdvm_f * lam[f]).real + (dSfdmdvm_f * lam[f + self.nbus]).imag
+ (dStdmdvm_f * lam[t]).real + (dStdmdvm_f * lam[t + self.nbus]).imag)
dSbusdmdvm[t, k_pos] = ((dSfdmdvm_t * lam[f]).real + (dSfdmdvm_t * lam[f + self.nbus]).imag
+ (dStdmdvm_t * lam[t]).real + (dStdmdvm_t * lam[t + self.nbus]).imag)
if k in self.br_mon_idx:
# Hessian terms, only for monitored lines
li = np.where(self.br_mon_idx == k)[0] # TODO: Why is this here?
dSfdmdm[k_pos, k_pos] = dSfdmdm_ * self.Sf[li].conj() * mu[li]
dStdmdm[k_pos, k_pos] = dStdmdm_ * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdmdva[f, k_pos] = dSfdmdva_f * self.Sf[li].conj() * mu[li]
dStdmdva[f, k_pos] = dStdmdva_f * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdmdva[t, k_pos] = dSfdmdva_t * self.Sf[li].conj() * mu[li]
dStdmdva[t, k_pos] = dStdmdva_t * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdmdvm[f, k_pos] = dSfdmdvm_f * self.Sf[li].conj() * mu[li]
dStdmdvm[f, k_pos] = dStdmdvm_f * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdmdvm[t, k_pos] = dSfdmdvm_t * self.Sf[li].conj() * mu[li]
dStdmdvm[t, k_pos] = dStdmdvm_t * self.St[li].conj() * mu[li + self.n_br_mon]
for k_pos, k in enumerate(self.k_tau):
f = self.from_idx[k]
t = self.to_idx[k]
Vf = self.V[f]
Vt = self.V[t]
Vmf = abs(Vf)
Vmt = abs(Vt)
mp = self.all_tap_m[k]
tau = self.all_tap_tau[k]
yk = ys[k]
tap = mp * np.exp(1j * tau)
tap_c = mp * np.exp(-1j * tau)
# Same procedure for phase controlled transformers
dSfdtdt_ = Vf * np.conj(yk * Vt) / tap
dStdtdt_ = Vt * np.conj(yk * Vf) / tap_c
dSfdtdva_f = - Vf * np.conj(yk * Vt) / tap
dSfdtdva_t = Vf * np.conj(yk * Vt) / tap
dStdtdva_f = - Vt * np.conj(yk * Vf) / tap_c
dStdtdva_t = Vt * np.conj(yk * Vf) / tap_c
dSfdtdvm_f = 1.0j * Vf / Vmf * np.conj(yk * Vt) / tap
dSfdtdvm_t = 1.0j * Vf / Vmt * np.conj(yk * Vt) / tap
dStdtdvm_f = -1.0j * Vt / Vmf * np.conj(yk * Vf) / tap_c
dStdtdvm_t = -1.0j * Vt / Vmt * np.conj(yk * Vf) / tap_c
# Merge Sf and St in Sbus
dSbusdtdt[k_pos, k_pos] = ((dSfdtdt_ * lam[f]).real + (dSfdtdt_ * lam[f + self.nbus]).imag
+ (dStdtdt_ * lam[t]).real + (dStdtdt_ * lam[t + self.nbus]).imag)
dSbusdtdva[f, k_pos] = ((dSfdtdva_f * lam[f]).real + (dSfdtdva_f * lam[f + self.nbus]).imag
+ (dStdtdva_f * lam[t]).real + (dStdtdva_f * lam[t + self.nbus]).imag)
dSbusdtdva[t, k_pos] = ((dSfdtdva_t * lam[f]).real + (dSfdtdva_t * lam[f + self.nbus]).imag
+ (dStdtdva_t * lam[t]).real + (dStdtdva_t * lam[t + self.nbus]).imag)
dSbusdtdvm[f, k_pos] = ((dSfdtdvm_f * lam[f]).real + (dSfdtdvm_f * lam[f + self.nbus]).imag
+ (dStdtdvm_f * lam[t]).real + (dStdtdvm_f * lam[t + self.nbus]).imag)
dSbusdtdvm[t, k_pos] = ((dSfdtdvm_t * lam[f]).real + (dSfdtdvm_t * lam[f + self.nbus]).imag
+ (dStdtdvm_t * lam[t]).real + (dStdtdvm_t * lam[t + self.nbus]).imag)
dSbusdtdt[k_pos, k_pos] = ((dSfdtdt_ * lam[f]).real + (dSfdtdt_ * lam[f + self.nbus]).imag
+ (dStdtdt_ * lam[t]).real + (dStdtdt_ * lam[t + self.nbus]).imag)
if k in self.br_mon_idx:
li = np.where(self.br_mon_idx == k)[0] # TODO: Why is this here?
dSfdtdt[k_pos, k_pos] = dSfdtdt_ * self.Sf[li].conj() * mu[li]
dStdtdt[k_pos, k_pos] = dStdtdt_ * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdtdva[f, k_pos] = dSfdtdva_f * self.Sf[li].conj() * mu[li]
dStdtdva[f, k_pos] = dStdtdva_f * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdtdva[t, k_pos] = dSfdtdva_t * self.Sf[li].conj() * mu[li]
dStdtdva[t, k_pos] = dStdtdva_t * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdtdvm[f, k_pos] = dSfdtdvm_f * self.Sf[li].conj() * mu[li]
dStdtdvm[f, k_pos] = dStdtdvm_f * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdtdvm[t, k_pos] = dSfdtdvm_t * self.Sf[li].conj() * mu[li]
dStdtdvm[t, k_pos] = dStdtdvm_t * self.St[li].conj() * mu[li + self.n_br_mon]
dSfdtdt[k_pos, k_pos] = dSfdtdt_ * self.Sf[li].conj() * mu[li]
dStdtdt[k_pos, k_pos] = dStdtdt_ * self.St[li].conj() * mu[li + self.n_br_mon]
return (dSbusdmdm, dSfdmdm, dStdmdm,
dSbusdmdvm, dSfdmdvm, dStdmdvm,
dSbusdmdva, dSfdmdva, dStdmdva,
dSbusdmdt, dSfdmdt, dStdmdt,
dSbusdtdt, dSfdtdt, dStdtdt,
dSbusdtdvm, dSfdtdvm, dStdtdvm,
dSbusdtdva, dSfdtdva, dStdtdva)
[docs]
def get_solution(self, ips_results: IpsSolution, verbose: int = 0, plot_error: bool = False):
"""
:param ips_results:
:param verbose:
:param plot_error:
:return:
"""
self.x2var(ips_results.x)
# Save Results DataFrame for tests
# pd.DataFrame(Va).transpose().to_csv('REEresth.csv')
# pd.DataFrame(Vm).transpose().to_csv('REEresV.csv')
# pd.DataFrame(Pg_dis).transpose().to_csv('REEresP.csv')
# pd.DataFrame(Qg_dis).transpose().to_csv('REEresQ.csv')
Pg = np.zeros(self.ngen + self.nsh)
Qg = np.zeros(self.ngen + self.nsh)
Pg[self.gen_disp_idx_sh] = self.Pg
Qg[self.gen_disp_idx_sh] = self.Qg
Pg[self.gen_nondisp_idx] = np.real(self.Sg_undis)
Qg[self.gen_nondisp_idx] = np.imag(self.Sg_undis)
# convert the Lagrange multipliers to significant ones
lam_p, lam_q = ips_results.lam[:self.nbus], ips_results.lam[self.nbus: 2 * self.nbus]
loading = np.abs(self.allSf) / (self.rates + 1e-9)
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
overloads_sf = (np.power(np.power(self.rates[self.br_mon_idx], 2) + self.sl_sf, 0.5)
- self.rates[self.br_mon_idx]) * self.Sbase
overloads_st = (np.power(np.power(self.rates[self.br_mon_idx], 2) + self.sl_st, 0.5)
- self.rates[self.br_mon_idx]) * self.Sbase
else:
overloads_sf = np.zeros_like(self.rates)
overloads_st = np.zeros_like(self.rates)
hvdc_power = self.nc.hvdc_data.Pset.copy()
hvdc_power[self.hvdc_disp_idx] = self.Pfdc
hvdc_loading = hvdc_power / (self.nc.hvdc_data.rates + 1e-9)
tap_module = np.zeros(self.nc.nbr)
tap_phase = np.zeros(self.nc.nbr)
tap_module[self.k_m] = self.tap_m
tap_phase[self.k_tau] = self.tap_tau
Pcost = np.zeros(self.ngen + self.nsh)
Pcost[self.gen_disp_idx_sh] = (self.c0 + self.c1 * Pg[self.gen_disp_idx_sh]
+ self.c2 * np.power(Pg[self.gen_disp_idx_sh], 2.0))
Pcost[self.gen_nondisp_idx] = (self.c0n + self.c1n * np.real(self.Sg_undis)
+ self.c2n * np.power(np.real(self.Sg_undis), 2.0))
nodal_capacity = self.slcap * self.Sbase
vsc_loading = np.zeros(self.nvsc)
if self.n_vsc_lim:
vsc_loading[self.vsc_lim_idx] = (self.It_vsc[self.vsc_lim_idx]
/ self.vsc_rate_pu[self.vsc_lim_idx])
tend = timeit.default_timer()
if self.options.verbose > 0:
df_bus = pd.DataFrame(data={'Va (rad)': self.Va, 'Vm (p.u.)': self.Vm,
'dual price (β¬/MW)': lam_p, 'dual price (β¬/MVAr)': lam_q})
df_gen = pd.DataFrame(data={'P (MW)': Pg * self.Sbase, 'Q (MVAr)': Qg * self.Sbase})
df_linkdc = pd.DataFrame(data={'P_dc (MW)': self.Pfdc * self.Sbase})
df_slsf = pd.DataFrame(data={'Slacks Sf': self.sl_sf})
df_slst = pd.DataFrame(data={'Slacks St': self.sl_st})
df_slvmax = pd.DataFrame(data={'Slacks Vmax': self.sl_vmax})
df_slvmin = pd.DataFrame(data={'Slacks Vmin': self.sl_vmin})
df_trafo_m = pd.DataFrame(data={'V (p.u.)': self.tap_m}, index=self.k_m)
df_trafo_tau = pd.DataFrame(data={'Tau (rad)': self.tap_tau}, index=self.k_tau)
# df_times = pd.DataFrame(data=times[1:], index=list(range(result.iterations)),
# columns=['t_modadm', 't_f', 't_g', 't_h', 't_fx', 't_gx',
# 't_hx', 't_fxx', 't_gxx', 't_hxx', 't_nrstep',
# 't_mult', 't_steps', 't_cond', 't_iter'])
print("Bus:\n", df_bus)
print("V-Trafos:\n", df_trafo_m)
print("Tau-Trafos:\n", df_trafo_tau)
print("Gen:\n", df_gen)
print("Link DC:\n", df_linkdc)
print('Qshunt min: ' + str(self.Qsh_min))
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
print("Slacks:\n", df_slsf)
print("Slacks:\n", df_slst)
print("Slacks:\n", df_slvmax)
print("Slacks:\n", df_slvmin)
if self.optimize_nodal_capacity:
df_nodal_cap = pd.DataFrame(data={'Nodal capacity (MW)': self.slcap * self.Sbase},
index=self.capacity_nodes_idx)
print("Nodal Capacity:\n", df_nodal_cap)
print("Error", ips_results.error)
print("Gamma", ips_results.gamma)
print("Sf", self.Sf)
# if self.options.verbose > 1:
# print('Times:\n', df_times)
# print('Relative times:\n', 100 * df_times[['t_modadm', 't_f', 't_g', 't_h', 't_fx', 't_gx',
# 't_hx', 't_fxx', 't_gxx', 't_hxx', 't_nrstep',
# 't_mult', 't_steps', 't_cond', 't_iter']].div(
# df_times['t_iter'],
# axis=0))
if plot_error:
ips_results.plot_error()
if not ips_results.converged or ips_results.converged:
for i in range(self.nbus):
if abs(ips_results.dlam[i]) >= 1e-3:
self.logger.add_warning('Nodal Power Balance convergence tolerance not achieved',
device_property="dlam",
device=str(i),
value=str(ips_results.dlam[i]),
expected_value='< 1e-3')
if abs(ips_results.dlam[self.nbus + i]) >= 1e-3: # TODO: What is the difference with the previous?
self.logger.add_warning('Nodal Power Balance convergence tolerance not achieved',
device_property="dlam",
device=str(i),
value=str(ips_results.dlam[i + self.nbus]),
expected_value='< 1e-3')
for pvbus in range(self.npv):
if abs(ips_results.dlam[2 * self.nbus + 1 + pvbus]) >= 1e-3:
self.logger.add_warning('PV voltage module convergence tolerance not achieved',
device_property="dlam",
device=str(self.pv[pvbus]),
value=str((ips_results.dlam[2 * self.nbus + 1 + pvbus])),
expected_value='< 1e-3')
for k in range(self.n_br_mon):
muz_f = abs(ips_results.z[k] * ips_results.mu[k])
muz_t = abs(ips_results.z[k + self.n_br_mon] * ips_results.mu[k + self.n_br_mon])
if muz_f >= 1e-3:
self.logger.add_warning('Branch rating "from" multipliers did not reach the tolerance',
device_property="mu Β· z",
device=str(self.br_mon_idx[k]),
value=str(muz_f),
expected_value='< 1e-3')
if muz_t >= 1e-3:
self.logger.add_warning('Branch rating "to" multipliers did not reach the tolerance',
device_property="mu Β· z",
device=str(self.br_mon_idx[k]),
value=str(muz_t),
expected_value='< 1e-3')
# VSC inequalities (-It<=0, It-rate<=0)
_vsc_tail = self.nvsc + self.n_vsc_lim + self.n_sl_vsc
for link in range(self.n_disp_hvdc):
muz_f = abs(ips_results.z[self.nineq - _vsc_tail - 2 * self.n_disp_hvdc + link]
* ips_results.mu[self.nineq - _vsc_tail - 2 * self.n_disp_hvdc + link])
muz_t = abs(
ips_results.z[self.nineq - _vsc_tail - self.n_disp_hvdc + link] * ips_results.mu[
self.nineq - _vsc_tail - self.n_disp_hvdc + link])
if muz_f >= 1e-3:
self.logger.add_warning('HVDC rating "from" multipliers did not reach the tolerance',
device_property="mu Β· z",
device=str(link),
value=str(muz_f),
expected_value='< 1e-3')
if muz_t >= 1e-3:
self.logger.add_warning('HVDC rating "to" multipliers did not reach the tolerance',
device_property="mu Β· z",
device=str(link),
value=str(muz_t),
expected_value='< 1e-3')
if self.options.acopf_mode == AcOpfMode.ACOPFslacks:
for k in range(self.n_br_mon):
if overloads_sf[k] > self.options.ips_tolerance * self.Sbase:
self.logger.add_warning('Branch overload in the from sense (MVA)',
device=str(self.br_mon_idx[k]),
device_property="Slack",
value=str(overloads_sf[k]),
expected_value=f'< {self.options.ips_tolerance * self.Sbase}')
if overloads_st[k] > self.options.ips_tolerance * self.Sbase:
self.logger.add_warning('Branch overload in the to sense (MVA)',
device=str(self.br_mon_idx[k]),
device_property="Slack",
value=str(overloads_st[k]),
expected_value=f'< {self.options.ips_tolerance * self.Sbase}')
for i in range(self.npq):
if self.sl_vmax[i] > self.options.ips_tolerance:
self.logger.add_warning('Overvoltage',
device_property="Slack",
device=str(self.pq[i]),
value=str(self.sl_vmax[i]),
expected_value=f'>{self.options.ips_tolerance}')
if self.sl_vmin[i] > self.options.ips_tolerance:
self.logger.add_warning('Undervoltage',
device_property="Slack",
device=str(self.pq[i]),
value=str(self.sl_vmin[i]),
expected_value=f'> {self.options.ips_tolerance}')
if verbose > 0:
if len(self.logger):
self.logger.print()
self.results = NonlinearOPFResults(Va=self.Va, Vm=self.Vm, S=self.Scalc,
Sf=self.allSf, St=self.allSt, loading=loading,
Pg=Pg[:self.ngen], Qg=Qg[:self.ngen], Qsh=Qg[self.ngen:],
Pcost=Pcost[:self.ngen],
tap_module=tap_module,
tap_phase=tap_phase,
hvdc_Pf=hvdc_power, hvdc_loading=hvdc_loading,
vsc_Pt=self.Pt_vsc, vsc_Qt=self.Qt_vsc,
vsc_Pf=self.Pf_vsc, vsc_It=self.It_vsc,
vsc_loading=vsc_loading,
lam_p=lam_p, lam_q=lam_q,
sl_sf=self.sl_sf, sl_st=self.sl_st, sl_vmax=self.sl_vmax,
sl_vmin=self.sl_vmin,
nodal_capacity=nodal_capacity,
error=ips_results.error,
converged=ips_results.converged,
iterations=ips_results.iterations)
return self.results