# 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 math
from matplotlib import pyplot as plt
from typing import List, Any
from VeraGridEngine.Simulations.results_table import ResultsTable
from VeraGridEngine.Simulations.results_template import ResultsTemplate, ResultsProperty
from VeraGridEngine.basic_structures import Vec, Mat, StrVec
from VeraGridEngine.enumerations import StudyResultsType, ResultTypes, DeviceType
from VeraGridEngine.Utils.Symbolic.symbolic import Var
[docs]
class SPlotInteractionHandler:
"""
Handles interactive annotations and hover events for S-Domain stability plots.
"""
__slots__ = ['sc', 'annot', 'fig', 'ax']
def __init__(self, sc: Any, annot: Any, fig: Any, ax: Any) -> None:
"""
SPlotInteractionHandler constructor.
"""
self.sc: Any = sc
self.annot: Any = annot
self.fig: Any = fig
self.ax: Any = ax
[docs]
def update_annotation(self, ind: dict) -> None:
"""
Updates the annotation text and position based on the hovered point.
"""
pos = self.sc.get_offsets()[ind["ind"][0]]
self.annot.xy = pos
text: str = f"Re={pos[0]:.2f}, Im={pos[1]:.2f}"
self.annot.set_text(text)
self.annot.get_bbox_patch().set_alpha(0.8)
[docs]
def on_hover(self, event: Any) -> None:
"""
Hover event callback logic.
"""
if event.inaxes == self.ax:
cont, ind = self.sc.contains(event)
if cont:
self.update_annotation(ind)
self.annot.set_visible(True)
self.fig.canvas.draw_idle()
else:
if self.annot.get_visible():
self.annot.set_visible(False)
self.fig.canvas.draw_idle()
else:
pass
else:
pass
[docs]
class SmallSignalStabilityRmsResults(ResultsTemplate):
"""
Small-signal Analysis results storage and visualization.
"""
LOCAL_RESULTS_DECLARATIONS = (
ResultsProperty(name='stat_vars_array', tpe=StrVec, old_names=list(), expandable=False),
ResultsProperty(name='eigenvalues', tpe=Vec, old_names=list(), expandable=False),
ResultsProperty(name='participation_factors', tpe=Mat, old_names=list(), expandable=False),
ResultsProperty(name='damping_ratios', tpe=Vec, old_names=list(), expandable=False),
ResultsProperty(name='conjugate_frequencies', tpe=Vec, old_names=list(), expandable=False),
ResultsProperty(name='state_matrix', tpe=Mat, old_names=list(), expandable=False),
)
__slots__ = [
'stat_vars_array', 'eigenvalues', 'participation_factors',
'damping_ratios', 'conjugate_frequencies', 'state_matrix'
]
def __init__(self,
eigenvalues: Vec,
participation_factors: Mat,
damping_ratios: Vec,
conjugate_frequencies: Vec,
state_matrix: Mat,
stat_vars: List[Var])-> None:
"""
Small-signal Analysis results
:param eigenvalues:
:param participation_factors:
:param damping_ratios:
:param conjugate_frequencies:
:param state_matrix:
:param stat_vars:
:param reduced_state_matrix:
"""
available_list: list = list([
ResultTypes.StateMatrix,
ResultTypes.Modes,
ResultTypes.ParticipationFactors,
ResultTypes.SDomainPlot,
ResultTypes.SDomainPlotHz
])
ResultsTemplate.__init__(
self,
name='Small Signal Stability',
available_results=available_list,
time_array=None,
clustering_results=None,
study_results_type=StudyResultsType.SmallSignalStability
)
names_list: list = list()
for i, var in enumerate(stat_vars):
names_list.append(f"{var}{i // 2 + 1}")
self.stat_vars_array: Vec = np.array(names_list, dtype=np.str_)
self.eigenvalues: Vec = eigenvalues
self.participation_factors: Mat = participation_factors
self.damping_ratios: Vec = damping_ratios
self.conjugate_frequencies: Vec = conjugate_frequencies
self.state_matrix: Mat = state_matrix
[docs]
def mdl(self, result_type: ResultTypes) -> ResultsTable:
"""
Export the results as a ResultsTable for plotting.
"""
if result_type == ResultTypes.StateMatrix:
return ResultsTable(
data=self.state_matrix,
index=np.array([f"Equation {i}" for i in range(len(self.eigenvalues))], dtype=np.str_),
columns=np.array(self.stat_vars_array.astype(str), dtype=np.str_),
title="State Matrix",
idx_device_type=DeviceType.NoDevice,
cols_device_type=DeviceType.NoDevice
)
elif result_type == ResultTypes.ParticipationFactors:
return ResultsTable(
data=self.participation_factors,
index=np.array(self.stat_vars_array.astype(str), dtype=np.str_),
columns=np.array([f"Mode {i}" for i in range(len(self.eigenvalues))], dtype=np.str_),
title="Participation factors for each eigenvalue",
idx_device_type=DeviceType.NoDevice,
cols_device_type=DeviceType.NoDevice
)
elif result_type == ResultTypes.Modes:
re: Vec = self.eigenvalues.real
im: Vec = self.eigenvalues.imag
data_modes: Mat = np.c_[re, im, self.damping_ratios, self.conjugate_frequencies]
return ResultsTable(
data=data_modes,
index=np.array([f"Mode {i}" for i in range(len(self.eigenvalues))], dtype=np.str_),
columns=np.array(["Real", "Imaginary", "Damping ratio", "Oscillation frequency"]),
title="Eigenvalues",
idx_device_type=DeviceType.NoDevice,
cols_device_type=DeviceType.NoDevice
)
elif result_type == ResultTypes.SDomainPlot:
re: Vec = self.eigenvalues.real
im: Vec = self.eigenvalues.imag
data: Mat = np.c_[re, im]
d: Vec = np.abs(np.nan_to_num(re))
colors: Vec = (-d / d.max())
slope: float = 1 / 0.05
x_z: Vec = np.linspace(-200, 0, 400)
y_z: Vec = slope * x_z
margin_x: float = (re.max() - re.min()) * 0.1
margin_y: float = (im.max() - im.min()) * 0.1
x_min: float = re.min() - margin_x
x_max: float = re.max() + margin_x
y_min: float = im.min() - margin_y
y_max: float = im.max() + margin_y
if self.plotting_allowed():
plt.ion()
fig: Any = plt.figure(figsize=(8, 6))
ax: Any = fig.add_subplot(111)
ax.plot(x_z, y_z, '--', color='grey', linewidth=0.7, alpha=0.6, label='ΞΆ = 5%')
ax.plot(x_z, -y_z, '--', color='grey', linewidth=0.7, alpha=0.6)
sc: Any = ax.scatter(re, im, c=colors, cmap='winter', s=120, alpha=0.8)
fig.suptitle("S-Domain Stability plot")
ax.set_xlabel(r'Real')
ax.set_ylabel(r'Imaginary [rad/s]')
ax.axhline(0, color='black', linewidth=1)
ax.axvline(0, color='black', linewidth=1)
plt.xlim([x_min, x_max])
plt.ylim([y_min, y_max])
plt.tight_layout()
plt.show()
annot: Any = ax.annotate("", xy=(0, 0), xytext=(20, 20),
textcoords="offset points",
bbox=dict(boxstyle="round", fc="w"),
arrowprops=dict(arrowstyle="->"),
fontsize=8)
annot.set_visible(False)
handler: SPlotInteractionHandler = SPlotInteractionHandler(sc, annot, fig, ax)
fig.canvas.mpl_connect("motion_notify_event", handler.on_hover)
else:
pass
return ResultsTable(data=data,
index=np.empty(len(self.eigenvalues), dtype=np.str_),
idx_device_type=DeviceType.NoDevice,
columns=np.array(['Real', 'Imag']),
cols_device_type=DeviceType.NoDevice,
title="S-Domain Stability plot"
)
elif result_type == ResultTypes.SDomainPlotHz:
re: Vec = self.eigenvalues.real
im: Vec = self.eigenvalues.imag / (2 * math.pi)
data: Mat = np.c_[re, im]
d: Vec = np.abs(np.nan_to_num(re))
colors: Vec = (-d / d.max())
slope: float = 1 / 0.05
x_z: Vec = np.linspace(-200, 0, 400)
y_z: Vec = slope * x_z / (2 * math.pi)
margin_x: float = (re.max() - re.min()) * 0.1
margin_y: float = (im.max() - im.min()) * 0.1
x_min: float = re.min() - margin_x
x_max: float = re.max() + margin_x
y_min: float = im.min() - margin_y
y_max: float = im.max() + margin_y
if self.plotting_allowed():
plt.ion()
fig: Any = plt.figure(figsize=(8, 6))
ax: Any = fig.add_subplot(111)
ax.plot(x_z, y_z, '--', color='grey',linewidth=0.7, alpha=0.6, label='ΞΆ = 5%')
ax.plot(x_z, -y_z, '--', color='grey',linewidth=0.7, alpha=0.6)
sc: Any = ax.scatter(re, im, c=colors, cmap='winter', s=120, alpha=0.8)
fig.suptitle("S-Domain Stability plot")
ax.set_xlabel(r'Real')
ax.set_ylabel(r'Imaginary [Hz]')
ax.axhline(0, color='black', linewidth=1) # eje horizontal (y = 0)
ax.axvline(0, color='black', linewidth=1)
plt.xlim([x_min, x_max])
plt.ylim([y_min, y_max])
plt.tight_layout()
plt.show()
annot: Any = ax.annotate("", xy=(0, 0), xytext=(20, 20),
textcoords="offset points",
bbox=dict(boxstyle="round", fc="w"),
arrowprops=dict(arrowstyle="->"),
fontsize=8)
annot.set_visible(False)
hz_handler: SPlotInteractionHandler = SPlotInteractionHandler(sc, annot, fig, ax)
fig.canvas.mpl_connect("motion_notify_event", hz_handler.on_hover)
else:
pass
return ResultsTable(data=data,
index=np.empty(len(self.eigenvalues), dtype=np.str_),
idx_device_type=DeviceType.NoDevice,
columns=np.array(['Real', 'Imag [Hz]']),
cols_device_type=DeviceType.NoDevice,
title="S-Domain Stability plot"
)
else:
raise Exception(f"Result type not understood: {result_type}")