Networks
Port and N-port network data, Touchstone I/O, embedded network blocks, and rational and state-space network models.
This page covers the network classes in witwin.maxwell (import witwin.maxwell as mw). Use them to read port results (PortData), work with S-matrices (NetworkData), read and write Touchstone files, embed a measured or fitted network in an FDTD run (NetworkBlock, TouchstoneNetwork), and fit rational or state-space models.
Conventions match Ports: peak phasors with exp(-i*omega*t) time dependence and Kurokawa power waves normalized to square-root watts:
a = (V + Z0*I) / (2*sqrt(2*Re(Z0)))
b = (V - conj(Z0)*I) / (2*sqrt(2*Re(Z0)))
|a|**2 - |b|**2 = 0.5*Re(V*conj(I))Network algebra uses PyTorch operations and keeps the autograd graph. File I/O (save, load, Touchstone) detaches tensors and moves them to CPU.
PortData#
@dataclass(frozen=True)
class PortData(port_name, frequencies, voltage, current, z0, direction='+',
reference_plane=None, available_power=None, mode_names=None, beta=None,
characteristic_impedance=None, tracking_confidence=None, metadata={},
phasor_convention=..., power_wave_convention=...)Frequency-domain voltage and current for one physical port. result.port(name) and result.ports return it. The last axis of every signal is frequency. Leading axes may index excitations (a PortSweep adds one) or modes (a WavePort adds a mode axis just before frequency).
Port name. Must not be empty.
Real, finite, non-negative tensor of shape [F] in hertz, on the same device as voltage.
Complex peak-phasor voltage in volts, last axis of length F.
Complex peak-phasor current in amperes, same shape, dtype, and device as voltage.
Reference impedance in ohms, broadcastable to the signal shape. Real part must be positive. Stored broadcast to the signal shape.
'+'"+" or "-".
NoneReference-plane coordinate in metres.
NoneReal available power in watts, broadcastable to the signal shape. Set only when the excitation defines a generator impedance.
NoneNames of the mode axis (second-to-last). Required when any modal field below is given.
NoneModal propagation constants, shape [M, F].
NoneModal characteristic impedance, shape [M, F], real part positive.
NoneReal mode-tracking confidence in [0, 1], shape [M, F].
{}Free-form metadata, copied to a plain dict.
PortData properties#
| Property | Value |
|---|---|
a, b | Incident and reflected power waves |
z_in | voltage / current |
incident_power, reflected_power | abs(a)**2, abs(b)**2 in watts |
accepted_power | incident_power - reflected_power |
delivered_power | 0.5 * Re(V * conj(I)) |
reflection_coefficient | b / a |
return_loss_db, return_loss | -20*log10(abs(b/a)) |
vswr | (1+abs(gamma))/(1-abs(gamma)), or inf when abs(gamma) is 1 or more |
PortData.from_power_waves#
PortData.from_power_waves(*, port_name, frequencies, a, b, z0, **kwargs) -> PortDataBuild a PortData from complex power waves a and b. Extra keyword arguments go to the constructor.
PortData.save#
PortData.save(path: str | Path) -> NoneSave a detached CPU snapshot with torch.save. Creates parent folders. metadata may hold only tensors and primitive dict, list, and tuple values.
PortData.load#
PortData.load(path: str | Path, map_location=None) -> PortDataLoad a snapshot written by save. No autograd graph is restored.
NetworkData#
@dataclass(frozen=True)
class NetworkData(frequencies, s, z0, port_names, valid_columns=None, metadata={},
phasor_convention=..., power_wave_convention=...)Complex N-port scattering data in [frequency, output_port, input_port] order, so b = S @ a. result.network returns it after a PortSweep.
Real, finite, non-negative tensor [F] in hertz, on the same device as s.
Complex, finite S-matrix of shape [F, N, N].
Reference impedance in ohms: a scalar, or shape [N], [1, N], [F, 1], or [F, N]. Stored as [F, N] with the dtype of s. Real part must be positive.
N unique, non-empty names without surrounding spaces, in matrix order.
NoneBoolean tensor [N] marking excitation columns that were run. None marks all valid. Most operations require a complete matrix and raise RuntimeError otherwise.
{}Free-form metadata. Transform methods append an entry to metadata["network_transform_history"].
import torch
import witwin.maxwell as mw
f = torch.linspace(1e9, 3e9, 21, dtype=torch.float64)
s = torch.zeros((21, 2, 2), dtype=torch.complex128)
s[:, 1, 0] = s[:, 0, 1] = 0.9
net = mw.NetworkData(frequencies=f, s=s, z0=50.0, port_names=("in", "out"))
print(net.return_loss_db.shape) # [21, 2]NetworkData.from_touchstone#
NetworkData.from_touchstone(path, *, device=None, dtype=torch.complex128) -> NetworkDataRead a Touchstone 1.x (.sNp) or 2.0 (.sNp or .ts) file with S, Z, or Y data. Tensors are created on device (CPU when None); dtype must be torch.complex64 or torch.complex128. Port names come from complete ! Port[n] = name comments, otherwise "1", "2", and so on. File details, comments, and parser warnings are stored in metadata["touchstone"]. Parsing is not differentiable.
Raises TouchstoneParseError for malformed content (with file and line), ValueError for an unsupported suffix or unreadable file.
NetworkData.from_z, NetworkData.from_y#
NetworkData.from_z(*, frequencies, z, z0, port_names, valid_columns=None, metadata=None,
phasor_convention=..., power_wave_convention=...) -> NetworkData
NetworkData.from_y(*, frequencies, y, z0, port_names, valid_columns=None, metadata=None,
phasor_convention=..., power_wave_convention=...) -> NetworkDataBuild a network from a complex [F, N, N] impedance (ohms) or admittance (siemens) matrix. Raises RuntimeError if valid_columns is incomplete.
NetworkData properties#
| Property | Value |
|---|---|
is_complete | True when every column is valid |
reflection_coefficient | Diagonal of s, shape [F, N] |
return_loss_db, return_loss | -20*log10(abs(S_ii)) |
vswr | VSWR of each diagonal entry |
NetworkData.to_z, NetworkData.to_y#
NetworkData.to_z() -> torch.Tensor
NetworkData.to_y() -> torch.TensorReturn the [F, N, N] impedance or admittance matrix.
NetworkData.renormalize#
NetworkData.renormalize(z0) -> NetworkDataReturn the network referenced to new impedances z0 (same shapes as the constructor).
NetworkData.terminate#
NetworkData.terminate(port, *, gamma=None, impedance=None) -> NetworkDataClose one port and return the reduced network. Give exactly one of gamma (reflection coefficient at the port reference plane) or impedance (load in ohms), each a scalar or a length-F tensor. port is a name or zero-based index. For a two-port terminated at port 2, S11 becomes S11 + S12*gamma*S21/(1 - S22*gamma).
NetworkData.cascade#
NetworkData.cascade(other: NetworkData, *, port_map: Mapping) -> NetworkDataConnect ports of this network to ports of other and remove them. port_map maps a self port (name or zero-based index) to an other port. The result lists this network's remaining ports, then the other's. Both networks need the same device, dtype, and frequency grid; connected ports need the same real reference impedance.
chain = net.cascade(net, port_map={"out": "in"})NetworkData.shift_reference_planes#
NetworkData.shift_reference_planes(distances, *, propagation_constants=None) -> NetworkDataMove port reference planes. distances is a scalar or [N] tensor in metres, positive outward. Each one-way wave is multiplied by exp(1j*propagation_constants*distances). propagation_constants is a scalar or broadcastable to [F, N] in rad/m; when None, the method reads metadata["propagation_constants"] and raises ValueError if it is missing.
NetworkData.to_mixed_mode#
NetworkData.to_mixed_mode(pairs, z0=None) -> NetworkDataConvert single-ended port pairs to differential and common modes. pairs is an iterable of (positive, negative) names or indices; each port may appear once. Outputs are ordered differential then common per pair, named d(p,n) and c(p,n), followed by unpaired ports. The basis is Vd = Vp - Vn, Vc = (Vp + Vn)/2, Id = (Ip - In)/2, Ic = Ip + In. With z0=None, paired ports must share a reference impedance and map to Zd = 2*z0 and Zc = z0/2.
NetworkData.validate_physicality#
NetworkData.validate_physicality(*, band=None, passivity_tolerance=1e-9,
causality_tolerance=1e-2) -> NetworkPhysicalityReportCheck sampled passivity (largest singular value of S at most 1) and, when the selected samples start at DC on a uniform grid, a finite-band negative-time energy test for causality. band is an optional (f_low, f_high) pair in hertz; at least two samples must fall inside it.
NetworkData.fit_rational#
NetworkData.fit_rational(config=None, *, representation='Y', initial_poles=None) -> RationalModelFit a shared-pole rational model to the "Y", "Z", or "S" representation. See fit_rational.
NetworkData.to_touchstone#
NetworkData.to_touchstone(path, *, format='ri', frequency_unit='hz', version='auto',
parameter='s') -> PathWrite the complete network to a Touchstone file and return the path.
Output file. A .sNp suffix must match the port count; .ts forces version 2.0.
'ri'"ri", "ma", or "db".
'hz'"hz", "khz", "mhz", or "ghz".
'auto'"auto", "1.0", or "2.0". "auto" writes 2.0 for a .ts path or per-port reference impedances, otherwise 1.0.
's'"s", "z", or "y".
Raises ValueError for incomplete columns, non-increasing frequencies, complex or frequency-dependent reference impedances, non-ASCII port names, or a version 1.0 file with unequal reference impedances.
net = mw.NetworkData.from_touchstone("filter.s2p")
net.renormalize(75.0).to_touchstone("filter_75.s2p", format="ma", frequency_unit="ghz")NetworkData.save, NetworkData.load#
NetworkData.save(path: str | Path) -> None
NetworkData.load(path: str | Path, map_location=None) -> NetworkDataSave or load a detached CPU snapshot with torch.save and torch.load(weights_only=True).
NetworkPhysicalityReport#
@dataclass(frozen=True)
class NetworkPhysicalityReportResult of NetworkData.validate_physicality.
| Field | Meaning |
|---|---|
frequency_band | (f_min, f_max) of the checked samples |
sample_count | Number of checked samples |
passive | Sampled passivity verdict |
passivity_margin | 1 - max singular value |
max_passivity_violation | max(0, -passivity_margin) |
stable | None; validate_physicality does not test stability |
causal | True, False, or None when indeterminate |
negative_time_energy_ratio | Fraction of impulse energy at negative time, or None |
passivity_tolerance, causality_tolerance | Tolerances used |
warnings | Explanatory messages |
The causality test is a finite-band diagnostic, not a certificate.
TouchstoneParseError#
class TouchstoneParseError(ValueError)Raised for malformed Touchstone content. Attributes: path (Path) and line_number (int). The message has the form "<path>: line <n>: <reason>".
NetworkBlock#
@dataclass(frozen=True)
class NetworkBlock(name, network, connections, fit=RationalFitConfig(), model=None,
extrapolation='reject', delay_seconds=None, max_delay_steps=65536)A passive network connected to named LumpedPort or TerminalPort ports in the scene and stepped with the FDTD fields. Add it with Scene(networks=(...)) or scene.add_network(block); read results with result.embedded_network(name).
Block name, non-empty without surrounding spaces.
The network data. Its port_names order is the network port order.
Maps every network port (by name or one-based index) to a distinct scene port name.
RationalFitConfig()Fit settings used at preparation. False requires model.
NonePre-fitted model. Requires fit=False. Its representation must be "Y", or "S" when delay_seconds is set, and its size must match the port count.
'reject'Only "reject" is supported.
NoneExplicit one-way delay per network port in seconds, "auto" to extract it, or None.
65536Positive upper bound on the delay line length in time steps.
Properties: port_order (the network's port_names) and connected_port_names (scene ports in that order).
Scene.add_network rejects unknown ports, a WavePort target, and a port already connected to another block. A connected port cannot also be excited or carry a termination. Automatic fitting and delay_seconds="auto" are not differentiable; for gradients pass a pre-fitted "Y" RationalModel with fit=False and train its residues and direct.
TouchstoneNetwork#
class TouchstoneNetwork(name, *, connections, network=None, path=None, fit=None, model=None,
extrapolation='reject', delay_seconds=None, max_delay_steps=65536,
device='cuda', dtype=torch.complex128)A NetworkBlock built from a Touchstone file or a NetworkData. Give exactly one of network or path. path is read with NetworkData.from_touchstone(path, device=device, dtype=dtype). fit=None uses RationalFitConfig(). Other arguments match NetworkBlock.
network = mw.TouchstoneNetwork(
name="filter",
path="filter.s2p",
connections={1: "input", 2: "output"},
fit=mw.RationalFitConfig(order=16),
device="cuda",
)
scene.add_network(network)EmbeddedNetworkData#
@dataclass(frozen=True)
class EmbeddedNetworkDataPer-frequency diagnostics of one embedded network, returned by result.embedded_network(name).
| Field | Shape and meaning |
|---|---|
name | Block name |
frequencies | [F] in hertz |
port_names | Network ports in network order |
voltage | [Nport, F] complex port voltage |
current | [Nport, F] complex current entering the network |
port_power | [Nport, F] power entering at each port, 0.5*Re(V*conj(I)) |
absorbed_power | [F], positive part of the net power entering |
generated_power | [F], positive part of the net power leaving |
state_norm | Scalar norm of the detached model state; a diagnostic only |
model_id | Identifier of the compiled model |
fit_report | FitReport or None |
runtime_warnings | Tuple of messages |
metadata | Dict with connections and conventions |
EmbeddedNetworkData.net_power#
EmbeddedNetworkData.net_power -> torch.TensorSigned total power entering the network, port_power.sum(dim=0), shape [F].
RationalFitConfig#
@dataclass(frozen=True)
class RationalFitConfig(order=8, band=None, weights=None, iterations=6,
enforce_stability=True, enforce_passivity=False,
relative_tolerance=1e-3, passivity_tolerance=1e-9,
enforcement_tolerance=1e-2, proportional=False,
diagnostic_only=False)Settings for shared-pole vector fitting. Pole relocation and passivity enforcement are not differentiable.
8Number of poles. Positive.
NoneIncreasing non-negative (f_low, f_high) in hertz. Only samples inside are fitted; at least order + 2 are needed.
NoneReal, finite, positive per-frequency weights of length F.
6Pole relocation iterations. 0 or more.
TrueReflect relocated poles into the stable half-plane.
FalseRun RationalModel.enforce_passivity after the fit.
1e-3Maximum allowed relative max error. Positive.
1e-9Passivity check tolerance. Non-negative.
1e-2Maximum relative change allowed by passivity enforcement. Non-negative.
FalseFit a proportional term s*E.
FalseReturn the model even when the error or enforcement tolerance is exceeded.
fit_rational#
fit_rational(frequencies, values, config=None, *, representation='Y',
initial_poles=None) -> RationalModelFit a real shared-pole transfer matrix by iterative vector fitting. The returned model's report is a NetworkFitReport.
Real, finite, non-negative [F] in hertz, with at least two samples. Must not require grad.
Complex samples of shape [F] or [F, Nout, Nin], on the same device. Must not require grad.
NoneFit settings. None uses RationalFitConfig().
'Y'"Y", "Z", or "S", recorded on the model.
NoneStarting poles of shape [config.order] in conjugate pairs. Must not require grad.
Raises RuntimeError when the fit has unstable poles, or (unless diagnostic_only) when the relative max error exceeds relative_tolerance or passivity enforcement changes the model by more than enforcement_tolerance.
model = mw.fit_rational(net.frequencies, net.to_y(), mw.RationalFitConfig(order=6))
print(model.report.relative_max_error)RationalModel#
@dataclass(frozen=True)
class RationalModel(poles, residues, direct=0.0, proportional=None,
representation='Y', report=None)Shared-pole real rational transfer matrix H(s) = sum_k R_k/(s - p_k) + D + s*E, evaluated at s = -i*2*pi*f.
Poles of shape [order], real or in conjugate pairs.
Residues [Nout, Nin, order], or [order] for a scalar response. Residues of real poles must be real; residues of conjugate poles must be conjugates.
0.0Real direct term, scalar or [Nout, Nin].
NoneReal proportional term E, scalar or [Nout, Nin]. None means zero.
'Y'"Y", "Z", or "S".
NoneFit report attached by fit_rational.
Properties: order, output_count, input_count, and is_stable (all poles have negative real part).
RationalModel.evaluate#
RationalModel.evaluate(frequencies) -> torch.TensorReturn the complex response [F, Nout, Nin] at non-negative frequencies in hertz.
RationalModel.to_state_space#
RationalModel.to_state_space(*, port_order=None, require_stable=True) -> StateSpaceNetworkBuild a real A/B/C/D realization. The model must be square with a zero proportional term. port_order defaults to ("1", "2", ...). Raises ValueError if the model is unstable and require_stable is True.
RationalModel.check_passivity#
RationalModel.check_passivity(frequencies, *, tolerance=1e-9) -> PassivityReportCheck passivity across the band spanned by frequencies with pole-aware interval bounds. An unstable model returns passive=False.
RationalModel.enforce_passivity#
RationalModel.enforce_passivity(frequencies, *, tolerance=1e-9) -> tuple[RationalModel, PassivityReport]Shift the direct term so a square "Y" or "Z" model becomes certified passive over the band. Returns the model unchanged if it already is. Raises RuntimeError if any parameter requires grad or certification fails.
RationalModel.save, RationalModel.load, RationalModel.fit#
RationalModel.save(path) -> None
RationalModel.load(path, *, map_location=None) -> RationalModel
RationalModel.fit(frequencies, values, config=None, *, representation='Y',
initial_poles=None) -> RationalModelsave and load write and read a detached snapshot. fit calls fit_rational.
StateSpaceNetwork#
@dataclass(frozen=True)
class StateSpaceNetwork(A, B, C, D, representation='Y', port_order=(),
passivity_margin=None, report=None)Continuous-time realization dx/dt = A x + B u, y = C x + D u. A, B, C, D are real floating tensors on one device with one dtype, shapes [Nstate, Nstate], [Nstate, Nin], [Nout, Nstate], [Nout, Nin]. port_order, when given, has one name per input.
Properties: state_count, input_count, output_count.
StateSpaceNetwork.evaluate#
StateSpaceNetwork.evaluate(frequencies) -> torch.TensorReturn C (sI - A)^-1 B + D at s = -i*2*pi*f, shape [F, Nout, Nin].
StateSpaceNetwork.check_passivity#
StateSpaceNetwork.check_passivity(frequencies, *, tolerance=1e-9) -> PassivityReportCertify passivity between samples with resolvent interval bounds.
StateSpaceNetwork.discretize#
StateSpaceNetwork.discretize(dt, *, stability_margin=1e-7) -> DiscreteStateSpaceNetworkCall bilinear_discretize(self, dt, stability_margin=stability_margin).
DiscreteStateSpaceNetwork#
@dataclass(frozen=True)
class DiscreteStateSpaceNetwork(A, B, C, D, dt, representation='Y', port_order=(),
pole_radius=0.0, passivity_margin=None, report=None)Discrete-time realization with step dt in seconds. pole_radius is the largest pole magnitude. Property: state_count.
DiscreteStateSpaceNetwork.step#
DiscreteStateSpaceNetwork.step(state, value) -> tuple[torch.Tensor, torch.Tensor]Return (A @ state + B @ value, C @ state + D @ value), the next state and the output.
bilinear_discretize#
bilinear_discretize(system: StateSpaceNetwork, dt: float, *,
stability_margin=1e-7) -> DiscreteStateSpaceNetworkDiscretize with the bilinear (trapezoidal) transform. dt is a positive finite time step in seconds; stability_margin lies strictly between 0 and 1.
Raises ValueError when the largest discrete pole magnitude is 1 - stability_margin or more.
check_sampled_passivity#
check_sampled_passivity(response, *, representation='Y', tolerance=1e-9) -> PassivityReportCheck passivity at the given samples only. response is complex [F, N, N]. For "Y" or "Z", the margin is the smallest eigenvalue of the Hermitian part; for "S", it is 1 - max singular value. passive is margin of at least -tolerance.
FitReport#
@dataclass(frozen=True)
class FitReportNumerical record of a rational fit.
| Field | Meaning |
|---|---|
rms_error, max_error | Absolute fit errors |
relative_rms_error, relative_max_error | Errors relative to the data scale |
frequency_band | (f_min, f_max) of the fitted samples |
order, iterations | Fit settings used |
unstable_poles | Count of poles with non-negative real part |
passivity_margin, max_passivity_violation | From the passivity check, or None |
condition_numbers | Least-squares condition numbers per solve |
enforcement_change, passivity_enforced | Effect of passivity enforcement |
differentiable_parameters | ("residues", "direct") |
warnings | Messages |
NetworkFitReport#
@dataclass(frozen=True)
class NetworkFitReport(FitReport)Multiport FitReport with extra fields: port_count, delay_seconds, delay_estimation_rank, delay_equation_count, delay_residual_seconds, delay_phase_error_degrees, and delay_reembedding_max_error.
PassivityReport#
@dataclass(frozen=True)
class PassivityReport(passive, margin, max_violation, tolerance, sample_count,
enforcement_change=0.0, certified=False, method='sampled')Result of a passivity check. certified is True when an interval bound proves the verdict between samples; method names the check used.