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_namestrrequired

Port name. Must not be empty.

frequenciestorch.Tensorrequired

Real, finite, non-negative tensor of shape [F] in hertz, on the same device as voltage.

voltagetorch.Tensorrequired

Complex peak-phasor voltage in volts, last axis of length F.

currenttorch.Tensorrequired

Complex peak-phasor current in amperes, same shape, dtype, and device as voltage.

z0float | complex | torch.Tensorrequired

Reference impedance in ohms, broadcastable to the signal shape. Real part must be positive. Stored broadcast to the signal shape.

directionstrdefault '+'

"+" or "-".

reference_planefloat | Nonedefault None

Reference-plane coordinate in metres.

available_powertorch.Tensor | Nonedefault None

Real available power in watts, broadcastable to the signal shape. Set only when the excitation defines a generator impedance.

mode_namestuple[str, ...] | Nonedefault None

Names of the mode axis (second-to-last). Required when any modal field below is given.

betatorch.Tensor | Nonedefault None

Modal propagation constants, shape [M, F].

characteristic_impedancetorch.Tensor | Nonedefault None

Modal characteristic impedance, shape [M, F], real part positive.

tracking_confidencetorch.Tensor | Nonedefault None

Real mode-tracking confidence in [0, 1], shape [M, F].

metadataMapping[str, Any]default {}

Free-form metadata, copied to a plain dict.

PortData properties#

PropertyValue
a, bIncident and reflected power waves
z_involtage / current
incident_power, reflected_powerabs(a)**2, abs(b)**2 in watts
accepted_powerincident_power - reflected_power
delivered_power0.5 * Re(V * conj(I))
reflection_coefficientb / 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) -> PortData

Build a PortData from complex power waves a and b. Extra keyword arguments go to the constructor.

PortData.save#

PortData.save(path: str | Path) -> None

Save 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) -> PortData

Load 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.

frequenciestorch.Tensorrequired

Real, finite, non-negative tensor [F] in hertz, on the same device as s.

storch.Tensorrequired

Complex, finite S-matrix of shape [F, N, N].

z0float | complex | torch.Tensorrequired

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.

port_namesIterable[str]required

N unique, non-empty names without surrounding spaces, in matrix order.

valid_columnstorch.Tensor | Nonedefault None

Boolean tensor [N] marking excitation columns that were run. None marks all valid. Most operations require a complete matrix and raise RuntimeError otherwise.

metadataMapping[str, Any]default {}

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) -> NetworkData

Read 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=...) -> NetworkData

Build a network from a complex [F, N, N] impedance (ohms) or admittance (siemens) matrix. Raises RuntimeError if valid_columns is incomplete.

NetworkData properties#

PropertyValue
is_completeTrue when every column is valid
reflection_coefficientDiagonal of s, shape [F, N]
return_loss_db, return_loss-20*log10(abs(S_ii))
vswrVSWR of each diagonal entry

NetworkData.to_z, NetworkData.to_y#

NetworkData.to_z() -> torch.Tensor
NetworkData.to_y() -> torch.Tensor

Return the [F, N, N] impedance or admittance matrix.

NetworkData.renormalize#

NetworkData.renormalize(z0) -> NetworkData

Return the network referenced to new impedances z0 (same shapes as the constructor).

NetworkData.terminate#

NetworkData.terminate(port, *, gamma=None, impedance=None) -> NetworkData

Close 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) -> NetworkData

Connect 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) -> NetworkData

Move 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) -> NetworkData

Convert 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) -> NetworkPhysicalityReport

Check 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) -> RationalModel

Fit 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') -> Path

Write the complete network to a Touchstone file and return the path.

pathstr | Pathrequired

Output file. A .sNp suffix must match the port count; .ts forces version 2.0.

formatstrdefault 'ri'

"ri", "ma", or "db".

frequency_unitstrdefault 'hz'

"hz", "khz", "mhz", or "ghz".

versionstrdefault 'auto'

"auto", "1.0", or "2.0". "auto" writes 2.0 for a .ts path or per-port reference impedances, otherwise 1.0.

parameterstrdefault '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) -> NetworkData

Save or load a detached CPU snapshot with torch.save and torch.load(weights_only=True).

NetworkPhysicalityReport#

@dataclass(frozen=True)
class NetworkPhysicalityReport

Result of NetworkData.validate_physicality.

FieldMeaning
frequency_band(f_min, f_max) of the checked samples
sample_countNumber of checked samples
passiveSampled passivity verdict
passivity_margin1 - max singular value
max_passivity_violationmax(0, -passivity_margin)
stableNone; validate_physicality does not test stability
causalTrue, False, or None when indeterminate
negative_time_energy_ratioFraction of impulse energy at negative time, or None
passivity_tolerance, causality_toleranceTolerances used
warningsExplanatory 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).

namestrrequired

Block name, non-empty without surrounding spaces.

networkNetworkDatarequired

The network data. Its port_names order is the network port order.

connectionsMapping[str | int, str]required

Maps every network port (by name or one-based index) to a distinct scene port name.

fitRationalFitConfig | Falsedefault RationalFitConfig()

Fit settings used at preparation. False requires model.

modelRationalModel | StateSpaceNetwork | Nonedefault None

Pre-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.

extrapolationstrdefault 'reject'

Only "reject" is supported.

delay_seconds'auto' | tuple[float, ...] | Nonedefault None

Explicit one-way delay per network port in seconds, "auto" to extract it, or None.

max_delay_stepsintdefault 65536

Positive 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 EmbeddedNetworkData

Per-frequency diagnostics of one embedded network, returned by result.embedded_network(name).

FieldShape and meaning
nameBlock name
frequencies[F] in hertz
port_namesNetwork 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_normScalar norm of the detached model state; a diagnostic only
model_idIdentifier of the compiled model
fit_reportFitReport or None
runtime_warningsTuple of messages
metadataDict with connections and conventions

EmbeddedNetworkData.net_power#

EmbeddedNetworkData.net_power -> torch.Tensor

Signed 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.

orderintdefault 8

Number of poles. Positive.

bandtuple[float, float] | Nonedefault None

Increasing non-negative (f_low, f_high) in hertz. Only samples inside are fitted; at least order + 2 are needed.

weightstorch.Tensor | Nonedefault None

Real, finite, positive per-frequency weights of length F.

iterationsintdefault 6

Pole relocation iterations. 0 or more.

enforce_stabilitybooldefault True

Reflect relocated poles into the stable half-plane.

enforce_passivitybooldefault False

Run RationalModel.enforce_passivity after the fit.

relative_tolerancefloatdefault 1e-3

Maximum allowed relative max error. Positive.

passivity_tolerancefloatdefault 1e-9

Passivity check tolerance. Non-negative.

enforcement_tolerancefloatdefault 1e-2

Maximum relative change allowed by passivity enforcement. Non-negative.

proportionalbooldefault False

Fit a proportional term s*E.

diagnostic_onlybooldefault False

Return the model even when the error or enforcement tolerance is exceeded.

fit_rational#

fit_rational(frequencies, values, config=None, *, representation='Y',
             initial_poles=None) -> RationalModel

Fit a real shared-pole transfer matrix by iterative vector fitting. The returned model's report is a NetworkFitReport.

frequenciestorch.Tensorrequired

Real, finite, non-negative [F] in hertz, with at least two samples. Must not require grad.

valuestorch.Tensorrequired

Complex samples of shape [F] or [F, Nout, Nin], on the same device. Must not require grad.

configRationalFitConfig | Nonedefault None

Fit settings. None uses RationalFitConfig().

representationstrdefault 'Y'

"Y", "Z", or "S", recorded on the model.

initial_polestorch.Tensor | Nonedefault None

Starting 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.

polestorch.Tensorrequired

Poles of shape [order], real or in conjugate pairs.

residuestorch.Tensorrequired

Residues [Nout, Nin, order], or [order] for a scalar response. Residues of real poles must be real; residues of conjugate poles must be conjugates.

directtensor | float | complexdefault 0.0

Real direct term, scalar or [Nout, Nin].

proportionaltensor | float | complex | Nonedefault None

Real proportional term E, scalar or [Nout, Nin]. None means zero.

representationstrdefault 'Y'

"Y", "Z", or "S".

reportFitReport | Nonedefault None

Fit 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.Tensor

Return 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) -> StateSpaceNetwork

Build 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) -> PassivityReport

Check 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) -> RationalModel

save 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.Tensor

Return 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) -> PassivityReport

Certify passivity between samples with resolvent interval bounds.

StateSpaceNetwork.discretize#

StateSpaceNetwork.discretize(dt, *, stability_margin=1e-7) -> DiscreteStateSpaceNetwork

Call 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) -> DiscreteStateSpaceNetwork

Discretize 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) -> PassivityReport

Check 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 FitReport

Numerical record of a rational fit.

FieldMeaning
rms_error, max_errorAbsolute fit errors
relative_rms_error, relative_max_errorErrors relative to the data scale
frequency_band(f_min, f_max) of the fitted samples
order, iterationsFit settings used
unstable_polesCount of poles with non-negative real part
passivity_margin, max_passivity_violationFrom the passivity check, or None
condition_numbersLeast-squares condition numbers per solve
enforcement_change, passivity_enforcedEffect of passivity enforcement
differentiable_parameters("residues", "direct")
warningsMessages

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.