Source code for VeraGridEngine.Simulations.EMT.JMARTI_Sim.jmarti_frequency_samples

# SPDX-License-Identifier: MPL-2.0

from __future__ import annotations

from typing import Sequence

import numpy as np


[docs] class JMartiFrequencySamples: """ Typed container holding frequency-domain line samples for one JMARTI fit. The fitting workflow starts from sampled multiconductor series impedance and shunt admittance matrices. The container validates all structural assumptions once so every later fitting stage can rely on a coherent input. """ __slots__ = ( "_frequency_hz", "_z_per_length", "_y_per_length", "_line_length_m", "_phase_labels", ) def __init__(self, frequency_hz: Sequence[float], z_per_length: np.ndarray, y_per_length: np.ndarray, line_length_m: float, phase_labels: Sequence[str]) -> None: """ Build one validated JMARTI frequency-sample container. :param frequency_hz: Strictly increasing frequency samples in Hz. :param z_per_length: Series impedance matrices with shape ``(nf, np, np)``. :param y_per_length: Shunt admittance matrices with shape ``(nf, np, np)``. :param line_length_m: Physical line length in meters. :param phase_labels: Ordered phase labels associated with the matrices. :return: None. """ self._frequency_hz: np.ndarray = np.asarray(frequency_hz, dtype=np.float64) self._z_per_length: np.ndarray = np.asarray(z_per_length, dtype=np.complex128) self._y_per_length: np.ndarray = np.asarray(y_per_length, dtype=np.complex128) self._line_length_m: float = float(line_length_m) self._phase_labels: tuple[str, ...] = tuple(str(label) for label in phase_labels) validate_jmarti_frequency_samples(self)
[docs] def get_frequency_hz(self) -> np.ndarray: """ Return the sampled frequency vector. :return: Frequency vector in Hz. """ return self._frequency_hz
[docs] def get_angular_frequency_rad_per_s(self) -> np.ndarray: """ Return the sampled angular-frequency vector. :return: Angular-frequency vector in rad/s. """ return 2.0 * np.pi * self._frequency_hz
[docs] def get_z_per_length(self) -> np.ndarray: """ Return the sampled series impedance matrices. :return: Complex impedance tensor with shape ``(nf, np, np)``. """ return self._z_per_length
[docs] def get_y_per_length(self) -> np.ndarray: """ Return the sampled shunt admittance matrices. :return: Complex admittance tensor with shape ``(nf, np, np)``. """ return self._y_per_length
[docs] def get_line_length_m(self) -> float: """ Return the physical line length in meters. :return: Line length in meters. """ return self._line_length_m
[docs] def get_phase_labels(self) -> tuple[str, ...]: """ Return the ordered phase labels. :return: Ordered phase-label tuple. """ return self._phase_labels
[docs] def get_frequency_count(self) -> int: """ Return the number of sampled frequencies. :return: Number of frequency samples. """ return int(self._frequency_hz.size)
[docs] def get_phase_count(self) -> int: """ Return the number of conductors or phases in the sample set. :return: Matrix dimension. """ return int(len(self._phase_labels))
[docs] def validate_jmarti_frequency_samples(samples: JMartiFrequencySamples) -> None: """ Validate one JMARTI frequency-sample container. :param samples: Frequency-sample container to validate. :return: None. :raises ValueError: If the sample set is structurally inconsistent. """ frequency_hz: np.ndarray = samples.get_frequency_hz() z_per_length: np.ndarray = samples.get_z_per_length() y_per_length: np.ndarray = samples.get_y_per_length() line_length_m: float = samples.get_line_length_m() phase_labels: tuple[str, ...] = samples.get_phase_labels() sample_count: int = int(frequency_hz.size) phase_count: int = int(len(phase_labels)) sample_index: int # Stage 1: validate the global dimensions of the sampled data. if sample_count >= 2: pass else: raise ValueError("JMARTI fitting requires at least two frequency samples") if line_length_m > 0.0: pass else: raise ValueError("JMARTI fitting requires a strictly positive line length") if phase_count >= 1: pass else: raise ValueError("JMARTI fitting requires at least one phase label") if z_per_length.shape == (sample_count, phase_count, phase_count): pass else: raise ValueError( "JMARTI series impedance samples must have shape " f"({sample_count}, {phase_count}, {phase_count}), got {z_per_length.shape}" ) if y_per_length.shape == (sample_count, phase_count, phase_count): pass else: raise ValueError( "JMARTI shunt admittance samples must have shape " f"({sample_count}, {phase_count}, {phase_count}), got {y_per_length.shape}" ) # Stage 2: require a strictly increasing frequency grid so all derivative and # phase-unwrapping steps later on remain well-posed. sample_index = 0 while sample_index < sample_count - 1: if frequency_hz[sample_index + 1] > frequency_hz[sample_index]: pass else: raise ValueError("JMARTI frequency samples must be strictly increasing") sample_index += 1 # Stage 3: ensure every sampled matrix stays finite and square. The fitting # pipeline can tolerate lossy or weakly coupled data, but not undefined values. if np.isfinite(z_per_length).all(): pass else: raise ValueError("JMARTI series impedance samples must be finite") if np.isfinite(y_per_length).all(): pass else: raise ValueError("JMARTI shunt admittance samples must be finite")
[docs] def build_jmarti_frequency_samples(frequency_hz: Sequence[float], z_per_length: np.ndarray, y_per_length: np.ndarray, line_length_m: float, phase_labels: Sequence[str]) -> JMartiFrequencySamples: """ Build one validated JMARTI frequency-sample container. :param frequency_hz: Strictly increasing frequency samples in Hz. :param z_per_length: Series impedance matrices with shape ``(nf, np, np)``. :param y_per_length: Shunt admittance matrices with shape ``(nf, np, np)``. :param line_length_m: Physical line length in meters. :param phase_labels: Ordered phase labels associated with the matrices. :return: Validated frequency-sample container. """ return JMartiFrequencySamples( frequency_hz=frequency_hz, z_per_length=z_per_length, y_per_length=y_per_length, line_length_m=line_length_m, phase_labels=phase_labels, )
[docs] def build_jmarti_frequency_subset(samples: JMartiFrequencySamples, low_hz: float, high_hz: float) -> JMartiFrequencySamples: """ Build one frequency-window subset of a JMARTI sample set. :param samples: Full sample set. :param low_hz: Lower retained frequency in Hz. :param high_hz: Upper retained frequency in Hz. :return: Windowed sample set. :raises ValueError: If the requested window leaves too few samples. """ frequency_hz: np.ndarray = samples.get_frequency_hz() selected_mask: np.ndarray = (frequency_hz >= float(low_hz)) & (frequency_hz <= float(high_hz)) if int(np.count_nonzero(selected_mask)) >= 2: pass else: raise ValueError("JMARTI frequency subset must keep at least two samples") return JMartiFrequencySamples( frequency_hz=frequency_hz[selected_mask], z_per_length=samples.get_z_per_length()[selected_mask, :, :], y_per_length=samples.get_y_per_length()[selected_mask, :, :], line_length_m=samples.get_line_length_m(), phase_labels=samples.get_phase_labels(), )