Source code for reasitic.network.sweep
"""Frequency-swept 2-port analysis.
Drives :func:`reasitic.network.spiral_y_at_freq` over a frequency
range and packages the per-frequency Y / Z / S / Pi results into a
:class:`NetworkSweep` object that can be exported to Touchstone or
inspected programmatically. Mirrors the binary's ``2Port`` /
``2PortX`` / ``2PortGnd`` family of commands (case 528, 539, 529).
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from reasitic.geometry import Shape
from reasitic.network.touchstone import TouchstonePoint
from reasitic.network.twoport import (
PiModel,
pi_equivalent,
spiral_y_at_freq,
y_to_s,
y_to_z,
)
from reasitic.tech import Tech
[docs]
@dataclass
class NetworkSweep:
"""A frequency sweep of 2-port parameters for a single shape."""
freqs_ghz: list[float]
Y: list[np.ndarray]
Z: list[np.ndarray]
S: list[np.ndarray]
pi: list[PiModel]
[docs]
def to_touchstone_points(self, *, param: str = "S") -> list[TouchstonePoint]:
"""Pack the sweep into :class:`TouchstonePoint` rows for export."""
if param == "S":
mats = self.S
elif param == "Y":
mats = self.Y
elif param == "Z":
mats = self.Z
else:
raise ValueError(f"unknown param {param!r}")
return [
TouchstonePoint(freq_ghz=f, matrix=m)
for f, m in zip(self.freqs_ghz, mats, strict=True)
]
[docs]
def two_port_sweep(
shape: Shape,
tech: Tech,
freqs_ghz: list[float],
*,
z0_ohm: float = 50.0,
) -> NetworkSweep:
"""Compute Y / Z / S / Pi at every frequency in ``freqs_ghz``.
``z0_ohm`` is used for the S-parameter conversion (defaults to
50 Ω, matching the binary's hardcoded reference).
"""
if not freqs_ghz:
raise ValueError("freqs_ghz must not be empty")
y_list: list[np.ndarray] = []
z_list: list[np.ndarray] = []
s_list: list[np.ndarray] = []
pi_list: list[PiModel] = []
y0 = 1.0 / z0_ohm
# Y can be singular for series-only spirals (no shunts) — Z then
# contains inf entries by design. Suppress the divide warnings.
with np.errstate(divide="ignore", invalid="ignore"):
for f in freqs_ghz:
Y = spiral_y_at_freq(shape, tech, freq_ghz=f)
y_list.append(Y)
z_list.append(y_to_z(Y))
s_list.append(y_to_s(Y, y0=y0))
pi_list.append(pi_equivalent(Y, freq_ghz=f))
return NetworkSweep(
freqs_ghz=list(freqs_ghz),
Y=y_list,
Z=z_list,
S=s_list,
pi=pi_list,
)
[docs]
def linear_freqs(start_ghz: float, stop_ghz: float, step_ghz: float) -> list[float]:
"""Generate an inclusive linear frequency list (the binary's stride)."""
if step_ghz <= 0:
raise ValueError("step_ghz must be positive")
if stop_ghz < start_ghz:
raise ValueError("stop must be >= start")
n = round((stop_ghz - start_ghz) / step_ghz) + 1
return [start_ghz + i * step_ghz for i in range(n)]