hermess.system ============== .. py:module:: hermess.system Attributes ---------- .. autoapisummary:: hermess.system.grid_sim hermess.system.dae_sim hermess.system.bus_init_sim hermess.system.line_sim hermess.system.disturbance_sim hermess.system.device_list_sim Classes ------- .. autoapisummary:: hermess.system.Grid hermess.system.GridSim hermess.system.Dae hermess.system.DaeSim Functions --------- .. autoapisummary:: hermess.system.stack_volt_power Module Contents --------------- .. py:class:: Grid .. py:attribute:: y_adm_matrix :type: Optional[casadi.SX] :value: None .. py:attribute:: y_series_r :type: Optional[casadi.SX] :value: None .. py:attribute:: y_series_i :type: Optional[casadi.SX] :value: None .. py:attribute:: G_dyn :type: Optional[casadi.SX] :value: None .. py:attribute:: B_i_dyn :type: Optional[casadi.SX] :value: None .. py:attribute:: B_j_dyn :type: Optional[casadi.SX] :value: None .. py:attribute:: X_dyn :type: Optional[casadi.SX] :value: None .. py:attribute:: y_series :type: Optional[numpy.ndarray] :value: None .. py:attribute:: z_imp_matrix :type: Optional[numpy.ndarray] :value: None .. py:attribute:: tau :type: Optional[numpy.ndarray] :value: None .. py:attribute:: tau_r :type: Optional[numpy.ndarray] :value: None .. py:attribute:: tau_i :type: Optional[numpy.ndarray] :value: None .. py:attribute:: incident_matrix :type: Optional[numpy.ndarray] :value: None .. py:attribute:: Bsum :type: Optional[numpy.ndarray] :value: None .. py:attribute:: Gsum :type: Optional[numpy.ndarray] :value: None .. py:attribute:: nb :type: int :value: 0 .. py:attribute:: nn :type: int :value: 0 .. py:attribute:: buses :type: list :value: [] .. py:attribute:: Bsum_trafo :type: Optional[numpy.ndarray] :value: None .. py:attribute:: Gsum_trafo :type: Optional[numpy.ndarray] :value: None .. py:attribute:: Sb :type: float :value: 100 .. py:attribute:: idx_i :type: list :value: [] .. py:attribute:: idx_j :type: list :value: [] .. py:attribute:: idx_i_re :type: list :value: [] .. py:attribute:: idx_j_re :type: list :value: [] .. py:attribute:: idx_i_im :type: list :value: [] .. py:attribute:: idx_j_im :type: list :value: [] .. py:attribute:: yinit :type: dict .. py:attribute:: yf :type: dict .. py:attribute:: sf :type: dict .. py:attribute:: line :type: Optional[hermess.devices.device.Line] :value: None .. py:attribute:: line_is_faulted :type: list[bool] :value: [] .. py:attribute:: line_is_open :type: list[bool] :value: [] .. py:attribute:: line_fault_adm :type: list[float] :value: [] .. py:attribute:: bus_is_faulted :type: list[bool] :value: [] .. py:attribute:: bus_fault_adm :type: list[float] :value: [] .. py:attribute:: idx_branch :type: dict[Tuple[str, str], int] .. py:attribute:: idx_bus :type: dict[str, int] .. py:attribute:: idx_bus_re :type: dict[str, int] .. py:attribute:: idx_bus_im :type: dict[str, int] .. py:attribute:: C_branches_forward :type: Optional[numpy.ndarray] :value: None .. py:attribute:: C_branches_reverse :type: Optional[numpy.ndarray] :value: None .. py:attribute:: C_branches :type: Optional[numpy.ndarray] :value: None .. py:attribute:: C_linecurrents_to_nodes :type: Optional[numpy.ndarray] :value: None .. py:method:: save_data(dae: Dae) -> None .. py:method:: init_symbolic(dae: Dae) -> None .. py:method:: gcall(dae: Dae) -> None .. py:method:: build_bus_rotation_T(dae: Dae) -> casadi.SX Builds rotation matrix in order to translate bus voltages. returns: Rotation matrix, SX (2*nn x 2*nn) .. py:method:: guncall(dae: Dae) -> None .. py:method:: add_lines(line: hermess.devices.device.Line) -> None .. py:method:: add_bus(bus: str, idx: list, idx_re: list, idx_im: list) -> None .. py:method:: build_Bsum_Gsum_trafo() -> None .. py:method:: build_y() -> None .. py:method:: _branch_selectors() -> dict Constant sparse selector matrices used to assemble the symbolic admittance and branch-current matrices without elementwise SX writes. ``ire``/``iim``/``jre``/``jim`` map branch k to the real/imag row of its from-/to-bus (shape 2·nn × nb); ``be``/``bo`` map branch k to its even/odd branch row (shape 2·nb × nb). Cached: they depend only on the topology, which is fixed after add_lines(). .. py:method:: build_y_sym(omega_buses: casadi.SX = ca.SX(1.0), omega_lines: float | casadi.SX = 1.0, dyn_update: bool = False) -> None .. py:method:: get_branch_index(node1: list[str], node2: list[str]) -> tuple[numpy.ndarray, numpy.ndarray] :param node1 (): List of starting nodes :param node2 (): List of receiving nodes Returns: The order of the given branch and the order of the opposite direction branch .. py:method:: get_node_index(buses: list) -> tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray] .. py:class:: GridSim Bases: :py:obj:`Grid` .. py:method:: gcall(dae: DaeSim, **kwargs) .. py:method:: init_from_power_flow(dae: DaeSim, static: hermess.devices.device.BusInit) -> None .. py:method:: print_init_power_flow(dae: DaeSim) -> None .. py:method:: build_Gsum() -> None .. py:method:: build_Bsum() -> None .. py:method:: build_incident_matrix() -> None .. py:method:: build_linecurrents_to_nodes() -> None Builds a constant mapping matrix that accumulates branch current states (xl) into nodal current balance entries (g). For each branch k with from-bus i and to-bus j: - Real current i_ijr contributes +1 at (vre_i) and -1 at (vre_j) - Imag current i_iji contributes +1 at (vim_i) and -1 at (vim_j) .. py:method:: build_linecurrents_transformed_to_bus_frame(dae: DaeSim) -> casadi.SX Builds the nodal current balance from line current states (xl) after rotating them from the line reference frame into each corresponding bus reference frame. For each branch k with from-bus i and to-bus j: - Real current i_ijr, transformed to reference frame of bus i, contributes +1 at (node i) - Real current i_ijr, transformed to reference frame of bus j, contributes -1 at (node j) - Imag current i_iji, transformed to reference frame of bus i, contributes +1 at (node i) - Imag current i_iji, transformed to reference frame of bus j, contributes -1 at (node j) - And applies (real) tap ratio at from side (i) :returns: (g_line) nodal current injection vector of length 2·nn .. py:method:: build_voltage_transformed_to_line_frame(dae: DaeSim) -> casadi.SX Builds the from- and to-terminal bus voltages expressed in the corresponding line reference frame. For each branch k with from-bus i and to-bus j: - Takes (V_i, V_j) given in the local bus frames of the from-bus i and to-bus j, respectively - Rotates both into branch k's line frame whose angle is defined by δ_line = 0.5(δ_i + δ_j) :returns: (Vi_re_L, Vi_im_L, Vj_re_L, Vj_im_L) transformed voltages as CasADi vectors of length nb .. py:method:: build_branch_current_fun(dae: DaeSim) -> None Builds a CasADi function that computes terminal branch currents from (x, y) consistent with distributed reference-frame formulation. For each branch k with from-bus i and to-bus j: - Takes terminal bus voltages (y) in the respective bus reference frames - Rotates Vi and Vj into the line reference frame of branch k - Applies the (real) tap ratio on the from-side (i) - Computes series and shunt currents (from transformed Vi,Vj) in the line frame - Forms terminal currents (If at i-side, It at j-side) in the line frame - Rotates If back to the i-bus frame and It back to the j-bus frame Stores: self._branch_current_fun(x, y) -> I_out, with length 4·nb ordered as [If_re, If_im, It_re, It_im] (in corresponding bus frames). .. py:method:: setup(dae: DaeSim, bus_init: hermess.devices.device.BusInit) -> None .. py:method:: update_effective_line_params() -> None Update Line object parameters to reflect current fault/open state. Uses saved originals so clearing a fault restores the original values. .. py:class:: Dae .. py:attribute:: nx :type: int :value: 0 .. py:attribute:: ny :type: int :value: 0 .. py:attribute:: nv :type: int :value: 0 .. py:attribute:: ng :type: int :value: 0 .. py:attribute:: nl :type: int :value: 0 .. py:attribute:: np :type: int :value: 0 .. py:attribute:: nts :type: int :value: 0 .. py:attribute:: n_priv :type: int :value: 0 .. py:attribute:: algebs_int_names :type: list[str] :value: [] .. py:attribute:: yinit_priv :type: list[float] :value: [] .. py:attribute:: x :type: Optional[casadi.SX] :value: None .. py:attribute:: y :type: Optional[casadi.SX] :value: None .. py:attribute:: f :type: Optional[casadi.SX] :value: None .. py:attribute:: g :type: Optional[casadi.SX] :value: None .. py:attribute:: fnode :type: Optional[casadi.SX] :value: None .. py:attribute:: xl :type: Optional[casadi.SX] :value: None .. py:attribute:: fl :type: Optional[casadi.SX] :value: None .. py:attribute:: p :type: Optional[casadi.SX] :value: None .. py:attribute:: p0 :type: Optional[casadi.SX] :value: None .. py:attribute:: s :type: Optional[casadi.SX] :value: None .. py:attribute:: x_full :type: Optional[numpy.ndarray] :value: None .. py:attribute:: y_full :type: Optional[numpy.ndarray] :value: None .. py:attribute:: i_full :type: Optional[numpy.ndarray] :value: None .. py:attribute:: T_start :type: float :value: 0.0 .. py:attribute:: T_end :type: float :value: 10.0 .. py:attribute:: time_steps :type: Optional[numpy.ndarray] :value: None .. py:attribute:: states :type: list[str] :value: [] .. py:attribute:: Sb :type: float :value: 100 .. py:attribute:: fn :type: Literal[50, 60] :value: 50 .. py:attribute:: t :type: float :value: 0.02 .. py:attribute:: omega_net :type: float :value: 1.0 .. py:attribute:: omega_coi :type: Optional[casadi.SX] .. py:attribute:: omega_ref :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_ref_buses :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_ref_lines :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_ref_expr :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_ref_buses_expr :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_ref_lines_expr :type: Optional[casadi.SX] :value: None .. py:attribute:: omega_mode :type: Literal['coi', 'single', 'nom', 'dist'] .. py:attribute:: has_delta_ref :type: bool :value: False .. py:attribute:: omega_single_idx :type: Optional[str] :value: None .. py:attribute:: xinit :type: list :value: [] .. py:attribute:: yinit :type: list :value: [] .. py:attribute:: iinit :type: list :value: [] .. py:attribute:: xlinit :type: list :value: [] .. py:attribute:: sinit .. py:attribute:: xmin :type: list :value: [] .. py:attribute:: xmax :type: list :value: [] .. py:attribute:: grid :type: Optional[Grid] :value: None .. py:attribute:: device_list :type: list :value: [] .. py:attribute:: bus_init :type: Optional[hermess.devices.device.BusInit] :value: None .. py:attribute:: incl_lim :type: bool .. py:attribute:: FG :type: Optional[casadi.Function] :value: None .. py:method:: __reduce__() .. py:method:: __setstate__(state) .. py:method:: setup(**kwargs) -> None .. py:method:: fgcall() -> None .. py:method:: init_symbolic() -> None .. py:method:: compute_coi_expr(device_list=None) -> None Compute the symbolic Centre-of-Inertia expression from devices. ``device_list`` defaults to ``self.device_list``; pass an explicit list to override. .. py:method:: update_omega() -> None Update system reference frequency to frequency of selected mode. - If omega_mode == 'coi': compute inertia-weighted COI of synchronous machines. - If omega_mode == 'single' get frequency of chosen synchronous machine or inverter. - If omega_mode == 'dist' get frequencies at all buses from frequency divider. - Otherwise: set ω_ref = ω_net (fallback behavior). .. py:method:: set_omega_single_idx_from_slack() -> None If omega_mode == 'single' and omega_single_idx is None, choose the device connected to the slack bus as reference. Uses ``self.bus_init`` and ``self.device_list`` bound to this Dae. If bus_init has no entries (e.g. the system does not declare a BusInit), falls back to the module-level ``bus_init_sim`` for the slack lookup — the slack bus is a property of the physical grid. .. py:method:: init_reference_frame_angle_states() -> None Initialize reference-frame angle states (δ_ref) for all network buses. - This function allocates one differential state per bus representing the local reference-frame angle δ_ref at that bus. - These angles are only needed for the distributed frequency reference framework (omega_mode == "dist"), specifically for transformations between the multiple local reference frames. .. py:attribute:: _LOAD_DEVICE_SHARES .. py:method:: _find_bus_load(bus_name: str) Return (device, k) for the load attached to *bus_name*, or raise. Iterates `self.device_list` and matches against any of the registered load device classes. The first matching (device, local-index) pair is returned. Single-load-per-bus is enforced upstream by the framework (one device per bus per type), so the first match is unambiguous. .. py:method:: dist_load(p: float, q: float, bus: list) -> None ZIP-aware LOAD disturbance. Increments the rated demand at the load attached to ``bus`` by (p, q) MW/MVAr. The increment is apportioned across the device's configured Z, I, P shares so the load preserves its voltage-dependence shape: * Z share contributes a linear admittance step (no 1/|V|² term), * I share contributes a rotated constant-current step, * P share contributes a constant-power step. For ``StaticLoadImpedance`` (z=1) and ``StaticLoadPower`` (p=1) the shares are inferred. With ``line_dyn=True`` the contributions feed ``dae.fnode`` via the ω_b / B_sum capacitor coupling; with ``line_dyn=False`` they feed ``dae.g`` directly. .. py:method:: check_disturbance(dist: hermess.devices.device.Disturbance, iter_forward: int) -> bool .. py:method:: exec_dist() .. py:method:: debug_check_initialization() -> None .. py:method:: check_initialization() -> None .. py:class:: DaeSim Bases: :py:obj:`Dae` .. py:attribute:: int_scheme_sim :value: None .. py:attribute:: int_scheme_sim_options :type: dict .. py:attribute:: eigenvalues :value: None .. py:attribute:: participation_factors :value: None .. py:attribute:: participation_factors_normalized :value: None .. py:attribute:: state_names :type: list :value: [] .. py:attribute:: modes :type: Optional[list] :value: None .. py:attribute:: small_signal_analysis :type: bool :value: False .. py:attribute:: t0 :type: float .. py:attribute:: tf :type: float .. py:attribute:: line_dyn :type: bool .. py:method:: init_symbolic() -> None .. py:method:: fgcall(tout: Optional[numpy.ndarray] = None) -> Optional[casadi.Function] Build the simulation integrator. With ``tout=None`` (the default) the integrator over ``(self.t0, self.tf)`` is built and stored as ``self.FG``. With an explicit output grid ``tout`` the integrator is built over ``(self.T_start, tout)`` and *returned* instead — used by the block-stepping limiter loop, which needs multi-step integrators next to the canonical single-step ``self.FG``. .. py:method:: _line_dyn_integrate(x0, y0, i0, s0, sl0, FG: Optional[casadi.Function] = None) Run the line_dyn integrator for one (set of) step(s) and return (x, y_full, i_line) as 2-D arrays (columns = time steps). Under line_dyn the differential block is [x | y_volt | xl]. With device private algebraics the voltages stay differential (driven by fnode) while the privates are algebraic: they are passed as z0 and returned in zf, and recombined here so y_full has the standard layout [voltages | privates]. ``FG`` overrides the integrator (block stepping); defaults to self.FG. .. py:method:: simulate(dist: hermess.devices.device.Disturbance) -> None .. py:method:: _snapshot_branch_params() -> dict Snapshot the current numeric branch admittance parameters from Grid.build_y(). .. py:method:: compute_i_full() -> None Compute branch currents from x_full and y_full in post-processing with vectorized NumPy. For has_delta_ref=False: matrix multiply C_branches @ y_full. For has_delta_ref=True: includes reference-frame rotation using delta_ref from x_full. .. py:method:: _compute_i_full_with_rotation(params: dict, start: int, end: int) -> None Compute branch currents with reference-frame rotation (has_delta_ref=True). .. py:method:: eigenvalue_analysis() -> None .. py:method:: _build_modes(participation_normalized: numpy.ndarray) -> list Collapse the raw eigenvalues into physical modes (complex-conjugate pairs merged) and attach the modal metrics used by the reports. Returns a list of mode dicts sorted by damping ratio (most critical first). Each carries the representative eigenvalue, natural frequency [Hz], damping ratio, and that mode's normalized participation column. .. py:method:: print_modal_report(top_k: int = 4) -> str Print a per-mode small-signal summary and return it as a string. One row per physical mode (complex-conjugate pairs collapsed), with the eigenvalue, natural frequency, damping ratio and the dominant states. Modes are sorted by damping ratio so the most critical appear first. .. py:method:: plot_eigenvalues(damping_ref: float = 0.05) -> None Simple eigenvalue (s-plane) scatter of the reduced state matrix. Every eigenvalue is plotted in the complex plane with the imaginary axis as the stability boundary. Points are colored red = unstable (Re>0), orange = lightly damped (ζ < ``damping_ref``), blue = well damped, grey = marginal (near the origin). A dashed wedge marks the constant-damping locus ζ = ``damping_ref``. When the spectrum is very wide (fast real modes far in the left half-plane) a second panel zooms in near the imaginary axis, where the slow/critical modes live. Unstable eigenvalues are annotated when there are only a few. .. py:method:: _render_participation_heatmap(modes: list, title: str, max_states: Optional[int] = 35, min_participation: float = 0.02, annotate: bool = True) -> None Render one participation-factor heatmap for a pre-selected, already ordered list of modes. Shared by the damping overview and the banded frequency views. Cells below ``min_participation`` are greyed out and states that never reach it (in the shown modes) are dropped; the row axis is capped at ``max_states`` strongest participants. .. py:method:: plot_participation_factors(max_modes: Optional[int] = 25, max_states: Optional[int] = 35, min_participation: float = 0.02, annotate: bool = True) -> None Single participation-factor heatmap of the most critical modes. Modes are sorted by damping ratio (most critical first) and the view is focused for legibility on large systems: only the first ``max_modes`` and the states that participate meaningfully in them are shown. Pass ``max_modes=None`` / ``max_states=None`` to show everything. For the frequency-banded views (one figure per band) use :meth:`plot_participation_bands`. .. py:method:: plot_participation_bands(max_states: Optional[int] = 35, min_participation: float = 0.02, annotate: bool = True) -> None Frequency-banded participation heatmaps — one figure per band. The modes are split into non-oscillatory (real) modes and four frequency bands; a figure is produced only for a band that actually contains modes. Within a band the modes are frequency-sorted (real modes are ordered by proximity to instability). Bands: real · 0–0.1 Hz · 0.1–2 Hz (electro- mechanical) · 2–50 Hz · >50 Hz. .. py:data:: grid_sim .. py:data:: dae_sim .. py:data:: bus_init_sim .. py:data:: line_sim .. py:data:: disturbance_sim .. py:data:: device_list_sim :value: [] .. py:function:: stack_volt_power(vre, vim) -> numpy.ndarray