Source code for VeraGridEngine.Devices.Injections.generator_q_curve

# 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 json
import numpy as np
from typing import Tuple, List
from matplotlib import pyplot as plt
from VeraGridEngine.basic_structures import Mat


[docs] class GeneratorQCurve: """ GeneratorQCurve """ __slots__ = ("_q_points") def __init__(self) -> None: # Array of points [(P1, Qmin1, Qmax1), (P2, Qmin2, Qmax2), ...] self._q_points: Mat = np.zeros((0, 3))
[docs] def get_data(self): """ Get the data :return: """ return self._q_points
[docs] def get_data_by_type(self): """ Get the data points P, Qmin, Qmax :return: P, Qmin, Qmax """ return self._q_points[:, 0], self._q_points[:, 1], self._q_points[:, 2]
[docs] def make_default_q_curve(self, Snom: float, Qmin: float, Qmax: float, n: int = 3): """ Compute the theoretical generator capability curve :param Snom: Nominal power :param Qmin: Minimum reactive power :param Qmax: Maximum reactive power :param n: number of points, at least 3 """ self._q_points = np.zeros((n, 3)) if n > 1: s2 = Snom * Snom Qmax2 = Qmax if Qmax < Snom else Snom Qmin2 = Qmin if Qmin > -Snom else -Snom # Compute the intersections of the Qlimits with the natural curve p0_max = np.sqrt(s2 - Qmax2 * Qmax2) p0_min = np.sqrt(s2 - Qmin2 * Qmin2) p0 = min(p0_max, p0_min) # pick the lower limit as the starting point for sampling # generate evenly spaced active power points from 0 to Snom self._q_points[1:, 0] = np.linspace(p0, Snom, n - 1) # enter the base points self._q_points[0, 0] = 0 self._q_points[0, 1] = Qmin2 self._q_points[0, 2] = Qmax2 for i in range(1, n): p2 = self._q_points[i, 0] * self._q_points[i, 0] # P^2 q = np.sqrt(s2 - p2) # point that naturally matches Q = sqrt(S^2 - P^2) # assign the natural point if it does not violates the limits imposes, else set the limit qmin = -q if -q > Qmin2 else Qmin2 qmax = q if q < Qmax2 else Qmax2 # Enforce that Qmax > Qmin if qmax < qmin: qmax = qmin if qmin > qmax: qmin = qmax # Assign the points self._q_points[i, 1] = qmin self._q_points[i, 2] = qmax else: self._q_points[0, 0] = 0 self._q_points[0, 1] = Qmin self._q_points[0, 2] = Qmax
[docs] def get_q_limits(self, p: float) -> Tuple[float, float]: """ Get the reactive power limits :param p: active power value (or array) :return: Qmin (float), Qmax (float) """ if self._q_points.shape[0] > 1: all_p = self._q_points[:, 0] all_qmin = self._q_points[:, 1] all_qmax = self._q_points[:, 2] qmin = np.interp(p, all_p, all_qmin) qmax = np.interp(p, all_p, all_qmax) return qmin, qmax else: return self._q_points[0, 1], self._q_points[0, 2]
[docs] def get_qmax(self, p: float) -> float: """ Get Qmax :param p: active power value in MW :return: Qmax in MVAr """ if self._q_points.shape[0] > 1: return np.interp(p, self._q_points[:, 0], self._q_points[:, 2]) else: return self._q_points[0, 2]
[docs] def get_qmin(self, p: float) -> float: """ Get Qmin :param p: active power value in MW :return: Qmin in MVAr """ if self._q_points.shape[0] > 1: return np.interp(p, self._q_points[:, 0], self._q_points[:, 1]) else: return self._q_points[0, 1]
def __str__(self) -> str: """ Get string representation of the curve :return: json string of list of lists: "[[P1, Qmin1, Qmax1], [P2, Qmin2, Qmax2], ...]" """ return self.str() def __eq__(self, other: "GeneratorQCurve") -> bool: """ Equality check :param other: GeneratorQCurve :return: equal? """ return np.allclose(self._q_points, other._q_points)
[docs] def to_list(self) -> list: """ Get list of points :return: """ return self._q_points.tolist()
[docs] def str(self) -> str: """ Get string representation of the curve :return: json string of list of lists: "[[P1, Qmin1, Qmax1], [P2, Qmin2, Qmax2], ...]" """ return json.dumps(self.to_list())
[docs] def parse(self, data: List[Tuple[float, float, float]]): """ Parse Json data :param data: List of lists with (latitude, longitude, altitude) """ if len(data) > 0: values = np.array(data) self.set(data=values) else: self._q_points = np.zeros((0, 4))
[docs] def set(self, data: np.ndarray): """ Parse Json data :param data: List of [(P1, Qmin1, Qmax1), (P2, Qmin2, Qmax2), ...] """ if data.ndim == 2: if data.shape[1] == 3: self._q_points = data else: raise ValueError('GeneratorQCurve data does not have exactly 3 columns') else: raise ValueError('GeneratorQCurve data must be 2-dimensional: (n_points, 3)')
[docs] def get_Qmin(self): return self._q_points[:, 1].min()
[docs] def get_Qmax(self): return self._q_points[:, 2].max()
[docs] def get_Pmin(self): return self._q_points[:, 0].min()
[docs] def get_Pmax(self): return self._q_points[:, 0].max()
[docs] def get_Snom(self): qmin = self._q_points[:, 1].min() qmax = self._q_points[:, 2].max() qfinal = max(abs(qmin), abs(qmax)) pmax = self._q_points[:, 0].max() return np.sqrt(pmax * pmax + qfinal * qfinal)
[docs] def plot(self, ax: plt.axis): """ :param ax: :return: """ x = self._q_points[:, 0] y1 = self._q_points[:, 1] y2 = self._q_points[:, 2] ax.plot(x, y1, color='red', marker='o', linestyle='solid', linewidth=2, markersize=4) ax.plot(x, y2, color='red', marker='o', linestyle='solid', linewidth=2, markersize=4) ax.set_xlabel("Q (MVAr)") ax.set_ylabel("P (MW)")
[docs] def copy(self) -> "GeneratorQCurve": elm = GeneratorQCurve() elm._q_points = self._q_points.copy() return elm