Waveguide ports and S-parameter extraction#

The port machinery is Magnelio’s largest methodological block. It combines standard 2D mode analysis with a family of exact discrete transparent boundary conditions (DTBC) that were derived in-house for the FIT leapfrog lattice; the published antecedents of each layer are listed per section.

2D port-mode solvers#

Curl-curl eigenvalue problem (TE/TM and hybrid modes)#

Port cross-section modes are computed from the generalised eigenvalue problem \(\mathbf K \hat e = \omega_c^2 \mathbf M \hat e\) with \(\mathbf K = \mathbf C_{2D}^{\mathsf T}\,\mathrm{diag}(M_\mu^{-1})\, \mathbf C_{2D}\), where \(\mathbf C_{2D}\) is the row/column slice of the 3D FIT curl matrix at the port plane (ports/modal/curl_curl_2d.py). Discretising the 2D problem in the same metric as the 3D problem (rather than with an independent 2D solver) is the property that makes the discrete mode an exact eigenvector of the 3D-restricted transversal operator; the FIT curl-curl eigenformulation itself is standard [2, 3]. The TM variant is the exact restriction of the same operator, an in-house derivation (DD-055).

TEM / quasi-TEM Laplace solver#

Multi-conductor cross-sections use the electrostatic route (ports/modal/tem_laplace.py): a 2D FIT Laplace problem \(\nabla\!\cdot(\varepsilon\nabla\varphi_k) = 0\) per signal conductor with unit-potential boundary conditions, \(\hat e_k = -\nabla\varphi_k\), line capacitance from the discrete energy. For inhomogeneous filling the quasi-TEM effective parameters follow the classic two-capacitance construction \(\varepsilon_{\text{eff}} = C'/C'_0\), \(Z_0 = 1/(c\sqrt{C' C'_0})\) — standard quasi-static transmission-line theory as found in the microwave-engineering literature [16, 17] (the specific formulation is textbook material; no single originating paper is claimed). The line-impedance definition used for TEM modes is the power–current impedance \(Z_{PI}\) (DD-025); the coexistence of \(Z_{PI}\), \(Z_{PV}\), \(Z_{VI}\) definitions is classical waveguide theory [16, 18]. Analytical reference modes for coaxial and rectangular-waveguide ports are closed-form textbook solutions [16].

The port report labels every solved impedance (on this grid): it is the value of the port-plane slice of the user’s mesh, not a converged 2D value — the ladder of refine_port_modes (section Converging the port cross-section below) converges it. An inhomogeneous cross-section adds quasi-static: the impedance is frequency-flat by construction, whereas the mode the grid actually carries disperses — its \(\varepsilon_{\text{eff}}\) rises with frequency and its impedance moves with it (by 3 to 7 % at 15 GHz on the tutorial microstrip) — and that difference, not the absorber, is where most of the \(-30\) dB-class \(|S_{11}|\) floor of the default pipeline comes from. The frequency dependence itself is available from the same report, report.dispersion(f_axis) (section Reference impedance, dispersion and renormalisation below).

Coupled lines. With more than one signal conductor above the ground — an edge-coupled microstrip pair, a stripline pickup, a multi-wire bus — the per-conductor Laplace solutions are the conductor basis of the line, not its mode basis: on an inhomogeneous cross-section every voltage pattern travels at its own speed, and only the eigen-patterns of the multiconductor telegrapher equations propagate without exchanging energy. The port therefore returns those eigen-patterns. From the per-conductor fields it forms the two per-unit-length capacitance matrices — \(\mathbf C\) with the actual dielectric and \(\mathbf C_0\) with the conductors in vacuum — and solves \(\mathbf C\,\mathbf v = \varepsilon_{\text{eff}}\, \mathbf C_0\,\mathbf v\), the quasi-static form of the modal decomposition of \(\mathbf L\mathbf C\) with \(\mathbf L = \mu_0\varepsilon_0\mathbf C_0^{-1}\) [19]. Each eigenvector is a conductor-voltage pattern (the even and odd modes of a symmetric pair), its eigenvalue the modal \(\varepsilon_{\text{eff}}\), and its impedance \(Z_0 = 1/(c\sqrt{C'_v C'_{0,v}})\) with the modal capacitances \(C'_v = \mathbf v^\top\mathbf C\,\mathbf v\) for the unit-Euclidean pattern \(\mathbf v\) — for a symmetric pair exactly \(Z_{0e}\) and \(Z_{0o}\). Dimensioning a coupled section from these two modes alone — a pair of lines, or one pair of a Lange coupler’s fingers — is the subject of the how-to guides Coupled-line directional coupler and Lange coupler. Channels are ordered by descending \(\varepsilon_{\text{eff}}\) and are orthogonal in the port’s capacitance-corrected mass. A single signal conductor is the \(1 \times 1\) case of the same construction. For a homogeneous filling the pencil degenerates (all patterns share one speed), and the channels are the capacitance-matrix eigenmodes of DD-066 instead.

Mode classification and multi-mode merge#

TE/TM and TEM/QTEM branches are merged into one unified multi-mode port; degenerate multi-TEM subspaces are orthonormalised through a Gram eigenbasis, and multi-channel projections use the dual basis (Gram inverse) (DD-066). Standard linear algebra.

Ports in absorbing walls#

A radiating structure is fed through a guide that reaches the domain wall: the neck of a horn, a coax entering the box. The wall is absorbing, the port sits in the guide’s cross-section on it (a window port with corners), and two things make that consistent (DD-198). The absorber is switched off in the columns behind the window over its whole depth, so the mode injected at the port travels a uniform, lossless guide into the domain and the reflected wave meets the port’s own termination — the transparent-boundary assumptions above hold because the feed is uniform there. And the window must be enclosed by conductor on the port slab: the lateral edge of the absorber switch-off then lies on metal, where it cannot scatter. A port that covers the whole absorbing face, or a window whose ring lies in free space, is refused with a pointer to these rules. A printed line — a microstrip feeding a patch array — satisfies them through a short shielded launch: two walls and a roof around the trace where it meets the wall, the window in the launch’s cross-section, the way a connector body encloses the line (how-to patch array). The far-field monitor knows about such feeds (far-field chapter).

Exact discrete transparent boundary conditions (DTBC)#

Scalar DTBC on uniform feed lines (TEM: DD-054, TE/TM: DD-055)#

On a uniform feed line, the longitudinal dynamics of one modal amplitude in the FIT leapfrog is exactly a 1D leapfrog discrete Klein–Gordon chain with modal Courant number \(r\) (read off the co-located pair product \(M_\varepsilon M_\mu\)) and mass \(q = \hat\omega_c \Delta t\) from the 2D eigenvalue (\(q=0\) for TEM). The exact transparent termination of the semi-infinite continuation is obtained by \(\mathcal Z\)-transform: the outgoing characteristic root \(\lambda(z)\) yields the ghost relation \(\hat u_{K+1} = \lambda(z)\,\hat u_K\), realised in time domain as a causal convolution over the boundary history whose kernel is computed by contour integration and auto-extended past the run length, making the boundary exact (reflection-free to machine precision) within any finite run (ports/modal/dtbc.py). Incident waves are prescribed at the ghost plane through the same kernel.

This construction was derived in-house for the FIT/FDTD leapfrog lattice (derivation in DD-054/DD-055; measured port floors −124 to −250 dB). The general concept of exact discrete transparent boundary conditions — deriving the boundary kernel from the discretised interior scheme rather than discretising a continuous ABC — is established in the numerical-analysis literature, e.g. by Arnold, Ehrhardt and Sofronov [20] and Ehrhardt’s work on discrete TBCs for Schrödinger-type equations [21]; discretely nonreflecting boundary closures for finite-difference schemes of linear hyperbolic systems are constructed by Rowley and Colonius [22]. None of these antecedents treats modal FIT/FDTD waveguide ports; that application appears to be original to Magnelio.

CW true-mode ports for inhomogeneous lines (DD-056)#

For inhomogeneous cross-sections measured at a single frequency, the true discrete modes of the z-uniform feed section are computed as eigenpairs of the quadratic ζ-pencil

\[ \left[\zeta^2 \mathbf D_{+1} + \zeta(\mathbf D_0 - \hat\sigma) + \mathbf D_{-1}\right]\varphi = 0 \]

built from the production system matrices at the port (ports/modal/zeta_pencil.py), solved by sparse shift-invert ARPACK on the linearised pencil. Quadratic eigenvalue problems and their linearisations are covered by the survey of Tisseur and Meerbergen [23]; computing waveguide Bloch/propagation modes from a transfer/period formulation is established practice in computational photonics and microwave theory (no single originating publication is claimed). The frequency-local closed-form \((r_\text{eff}, q_\text{eff})\) chain fit (Hellmann–Feynman derivative matching so the scalar DTBC is exact at the drive frequency) is an in-house derivation.

Galerkin band-subspace DTBC for broadband runs (DD-057)#

For pulsed broadband runs on inhomogeneous lines, the tracked mode-family traces over the band span a low-rank W-orthonormal subspace; the exterior half-line is Galerkin-projected onto it, inheriting the palindromic symmetry that makes the projected lattice lossless, and is closed by the exact small-system DTBC kernel (ports/modal/band_dtbc.py). Projection-based model order reduction (Galerkin projection onto an SVD/POD subspace) is standard numerical practice [24] (survey reference; the specific passive-by-construction boundary closure is in-house).

Which termination a channel gets, and how to see it#

The exact termination is not a setting — it is a certificate. A channel earns it when the meshed feed section really is the uniform discrete chain the derivation above assumes, which is tested in two stages before the run: the pair-product gate measures how far the co-located products \(M_\varepsilon M_\mu\) spread across the feed cross-section (a weighted RMS), and the slab gate compares every mass entry on the port plane with its continuation into the first feed cells. A channel that fails either one keeps working — it falls back to the modal Mur absorber — but its reflection floor rises from below \(-100\) dB to the order of \(-30\) dB.

The threshold of the first gate is a reflection budget (DD-229). The termination is exact for the weighted-mean chain, so a spread \(\delta\) across the cross-section leaves a residual mismatch, and that mismatch was measured through the production solver on three fixtures and two shapes of defect. The worst case rises linearly, at about a seventh of the spread; the gate uses the cruder bound \(|\Gamma| \le \delta\) and accepts up to \(\delta = 2\cdot 10^{-6}\), so a certified channel contributes at most \(-114\) dB — well below the acceptance line the port floors are held to. Every channel therefore publishes what its own cross-section costs it:

\[ \texttt{chain\_floor\_db} = 20\log_{10}\delta . \]

The shape of the non-uniformity matters more than its size, which is why the bound is deliberately crude. A smooth transversal tilt is antisymmetric against a symmetric mode, its first-order overlap vanishes, and the reflection falls to second order; a localised defect — one cell of the cross-section out of step, which is what geometric tolerance produces — keeps first order and reflects orders of magnitude more at the same measured spread. A single scalar cannot tell the two apart, so the gate assumes the worse one.

Failing a gate is two different events, and the reporting separates them. A cross-section that was meant to be uniform and missed the budget is the surprising one, and it warns: the port, the channel, the measured deviation, and where the mesh can explain it, the conformal ladder behind the offending faces (DD-228). A cross-section that is genuinely inhomogeneous is not a defect; a quasi-TEM line deviates at the material-contrast level and never qualified for the scalar chain in the first place, so the answer there is a different port model, not a mesh fix, and Magnelio does not editorialise about it every run.

That second case is worth stating plainly, because the default hides how much is on the table. On an inhomogeneous line the default port_model="modal" terminates with the first-order absorber and floors around \(-26\) to \(-39\) dB. The same line on port_model="auto" — which builds the certificates, sees the fallback and switches the run to the band pipeline — floors at \(-147\) to \(-168\) dB on a shielded microstrip. So the default gives up 60 to 110 dB of reflection floor for the cost of the band pipeline — the low end on a long run, the high end on a short one; if the port matters, change it.

That spread is the second thing to know about the band floor: how deep it lands depends on how long the run is. The exact boundary carries the whole recorded history rather than a few past steps, and its floor drifts upward as that history grows — measured at roughly five to seven decibels per doubling of the number of time steps. The figures above are what a run of the length the port floors are certified at reaches; a much longer broadband sweep on the same model floors tens of decibels above them.

Changing the port model changes more than the floor, and the differences are worth knowing before the run rather than after. The band pipeline tracks the mode family over a band, but the measurement axis is not confined to that band. On a quasi-TEM line the fundamental is the static mode of the cross-section, so its excitation direction is continued below the tracked band and closed with the static field solution; the drive then needs no roll-off room at the bottom and the pulse duration follows the measurement span instead of \(1/f_\text{start}\). A default axis — which starts at \(f_\text{max}/n_\text{freq}\) — runs.

Two limits remain. The reflection floor of the boundary degrades toward DC: on a coarse two-port fixture the same run measures \(-91\) dB at 34 MHz against \(-117\) to \(-140\) dB over the rest of the axis, so a measurement whose lowest points carry the answer should still not start lower than it needs to. And a fundamental with a cut-off — a hollow waveguide mode — has no static limit to be continued to, so there the pulse duration still grows as \(1/f_\text{start}\) and the auto-sizing refuses an axis that would make the run disproportionate, naming the axis start that fits.

The record has a fixed length, so energy_stop_db and signal-decay stops do not apply to it. The recorded channels are subspace projections whose incident/outgoing split is defined per frequency, so result.a() and result.b() are unavailable; the S-parameters and the raw V/I are. Everything else is unchanged — including writing to a project store, from which the S-matrix is re-derived on read exactly as on the default path, and continuing an interrupted run with resume(). The absorbing boundary here remembers the whole record rather than a few past steps, so its memory is checkpointed in full; a continued run is bit-identical to an uninterrupted one of the same length.

None of this has to be looked up. Whenever a run terminates a channel with the Mur fallback, the analysis prints a short balance sheet once: which channels fell back and what the cross-section measured, the floor they trade away against the runtime and the power waves they keep, and what port_model="band" costs — a few lines; the detail is this section.

Either way the decision is published per channel, so it can be inspected before a run is paid for:

for name, report in analysis.solve_ports().items():
    for mode in report.modes:
        print(name, mode.name, mode.termination,
              mode.chain_spread, mode.chain_floor_db)

termination is "dtbc" or "mur" and chain_spread is the cross-section measurement behind that decision. chain_floor_db is the reflection bound the spread implies, and it is published only where it means something: it is None on a channel the first-order absorber terminates, because there the floor is the absorber’s and not the cross-section’s, and None again where the measurement does not apply at all (a mode with a closed-form field evaluator is ineligible by construction). So on an inhomogeneous line, read chain_spread — the number that explains why the channel fell back — and take the floor from the absorber. print(report) shows the same on one line per mode.

The usual cause of a withheld certificate is a feed that is not translation-invariant along the port normal — a taper, a bend or a dielectric step too close behind the port plane. The remedy is to move the port back into the uniform part of the feed. Where the feed is uniform by construction and the gate still trips, the deviation is geometric tolerance in the solid: a body built by mirroring or unioning carries a looser tolerance than the shape it was built from, and the conformal cross-section inherits it.

Excitation and recording#

Port excitation prescribes the incident modal amplitude at the ghost plane (DTBC branch) or injects on the port plane (Mur branch); V/I are recorded on a single co-located plane (DD-041). Time signals are smooth pulses (Gaussian and derived shapes, signals/waveforms.py) — standard practice [5].

What the port launches: frozen or dispersive#

By default a port imprints one mode profile with one propagation delay — the quasi-static profile its Laplace solve produced, held fixed across the band. On a homogeneously filled cross-section that is exact: a hollow waveguide’s \(\sin(\pi x/a)\) does not move with frequency, however strongly its propagation constant does. On an inhomogeneous cross-section — a microstrip, a layered or a partially filled line — the mode the grid carries changes shape with frequency, and the frozen profile launches a field the line cannot support unchanged. The mismatch radiates into evanescent modes at the port and returns as a reflection the structure never produced.

port_source="dispersive" replaces it with a low-rank family: the true mode is solved along the band, decomposed into a handful of profiles (two or three suffice; the rank is chosen against a profile-error target and grows per channel, since the even and odd modes of a coupled pair do not need the same), and each profile is driven by its own waveform. The launched field is then the mode the grid actually carries at every frequency.

result = mio.AnalysisScatteringTD(
    mesh=mesh,
    port_source="dispersive",
).run(excited=["port1"])

What it buys and where. On a straight 50 Ω microstrip, whose true reflection is zero, the reported worst \(|S_{11}|\) falls from about −32.9 dB to −38.9 dB — the remaining reflection is the far port’s absorbing floor, not the launch. On a hollow guide it buys nothing, because there is nothing to repair. It costs roughly 1 % of the marching time per rank term.

Why it is one switch and not two. The launched profile and the frequency-flat impedance used to split incident from reflected waves are the same defect seen twice, and on a microstrip they stand nearly in antiphase — repairing either alone increases the reported reflection, because the accidental cancellation between them is lost. port_source="dispersive" therefore also selects the per-frequency decomposition (the one a band run uses), so the two are always repaired together. This is why the option changes how S-parameters are read as well as what the port emits.

Limits. The option applies to the modal Mur branch; a channel terminated by an exact DTBC prescribes its own incident wave and is left alone. A channel with no propagating mode over the band keeps the frozen source and says so. The per-frequency decomposition slightly overshoots unity transmission on a lossless line (about 0.3 % with the frozen source, 0.8 % with the dispersive one, growing with frequency); this is a property of the decomposition rather than of the source, and it does not depend on the rank.

S-parameters#

S-parameters are computed as power waves with \(\sqrt{\mathrm W}\) normalisation, \(a,b = (V \pm Z_0 I)/(2\sqrt{Z_0})\) per mode, from the recorded modal V/I after Fourier transform (postprocessing/modal_sparameters.py, DD-042/DD-078). The power-wave formalism is Kurokawa’s [27]. Two discrete-exactness refinements are in-house (DD-063, DD-056): the a/b split de-staggers E and H with the exact discrete factor \(\lambda^{1/2}(z)\) and uses the exact discrete wave impedance of the leapfrog chain (rather than the continuum \(Z_0\)), which removes the \(O(\beta\Delta z)\) staggering leak; and CW measurements solve the exact 2×2 phasor system per port (lock-in demodulation of incident and reflected discrete waves).

Excitation amplitudes are pinned to physical units at the source (C = 1 convention, DD-085), so recorded V/I and monitor fields are in SI units — an in-house calibration convention.

Reading an S-matrix#

Three pictures beyond the magnitude over frequency, each answering a different question about the same matrix.

plot_balance() sums \(\sum_i |S_{ij}|^2\) over the observed channels for every excitation \(j\): the share of the incident power that comes back out of the ports. On a lossless, fully exported network it is one, and what is missing left the ports — as ohmic and dielectric loss, or as radiation. It is therefore two instruments at once. On a closed structure it is a convergence check: a balance that misses one by a part in a thousand is telling you about the mesh, the port floor, or a run cut short, not about physics. On an antenna it is the radiated power, and deficit=True plots exactly that, in dB.

Two things it cannot check for you. Only channels present in the result are summed, so a network whose higher modes were not all exported reads short and its deficit looks like loss; is_complete says whether every observed channel was also excited. And a channel below its cut-on is NaN — an evanescent channel carries no active power, so it counts as zero rather than poisoning the sum.

plot_smith() traces the reflection channels on a Smith chart, where the impedance is readable off the circles of constant normalised resistance and reactance and a matched frequency sits at the centre. The chart holds against one normalisation. A dispersive waveguide mode’s reference impedance moves with frequency, so its circles hold nowhere in particular; call renormalize(50) first, and a warning says so when you have not.

plot_polar() is the same trajectory without the impedance grid, for the channels a Smith chart does not describe — transmission above all, whose magnitude is not bounded by one.

All three take the same channel arguments as plot_s and accept ax= to compose a figure; mark=[f1, f2, …] puts labelled dots on the named frequencies of a trace, which is how a resonance or a band edge gets pointed at.

Running out of time: continuing a truncated record#

A march has to stop somewhere. On a high-Q structure what it leaves behind is a record still ringing at the last step, and the Fourier transform reads that edge as content: truncation ripple over every S-parameter. Spending more steps is the honest fix and the library will do it (energy_stop_db, port_signal_stop_db), but a cavity whose decay time is thirty times the affordable march cannot be waited out at any price.

Past the excitation, though, the record is no longer being driven: it is the structure’s own free decay, a sum of damped exponentials whose poles are its resonances. result.extrapolate() fits those poles — a matrix pencil on the Hankel matrix of the record, all channels of one excitation sharing one pole set, poles outside the unit circle discarded as impossible for a passive decay — continues every recorded V and I until the model has died away, and recomputes the S-matrix from the continued records through the unchanged pipeline. The original result is untouched.

This is a model, not a measurement, and it is trustworthy exactly as far as the tail really is a free decay of a few resonances. The report in extrapolation gives the number that says so: the model fitted on the first half of its fit window, measured against the recorded second half. Below about \(10^{-2}\) the poles are the structure’s; approaching one, the fit is describing noise — or a delay line, whose \(e^{-2j\beta L}\) has infinitely many poles and whose record is a train of echoes rather than a ringing. Above 0.3 the call says so out loud; and because a failed fit produces a continuation that decays immediately, it leaves the result alone rather than corrupting it.

Nothing applies this automatically. An extrapolated resonance mistaken for a measured one is exactly what the truncation warning exists to prevent, so the warning names the method and the method stays a deliberate call.

Measured on an iris-coupled cavity fed through a slot below cut-off — lossless with one port, so \(|S_{11}| = 1\) is an exact reference: a 9.1 ns march reports a unitarity defect of 0.49, and its continuation 0.0073. Worth knowing from the same measurement: the raw defect grows with the length of the march, because a short run has barely filled the cavity yet. Truncation error is not monotone in run length, so “run longer and see whether it moves” is a poor convergence test here.

Band ports: one decomposition per frequency (DD-235)#

A band-subspace port does not record modal amplitudes whose incident/outgoing split is fixed once for the run. Its recording channels are subspace projections, and the split is defined per frequency: at every point of the measurement axis the true discrete modes of the port cross-section are solved on the stored inward chain (the ζ-pencil above), their exact V/I responses through the port’s fixed recording profiles are synthesised, and the recorded spectra are decomposed by one joint least-squares system written over all channels \(c\) and all modes \(j\) propagating at that frequency at once,

\[ V_c(f) = \sum_j a_j v^{\text{in}}_{cj} + b_j v^{\text{out}}_{cj}, \qquad I_c(f) = \sum_j a_j i^{\text{in}}_{cj} + b_j i^{\text{out}}_{cj}, \]

with \(S_{c} = b_c / a_\text{excited}\). Solving the channels jointly rather than one at a time is what makes the extraction exact: the recording profiles are frozen when the port is built and are not the modes of any single frequency, so every channel sees a mixture of every mode present, and only the joint system unmixes them — provided the basis it is written in contains them all.

Three things are worth keeping apart. A channel is a recording slot of the port, one per requested mode (n_modes), with a profile fixed at build time. A propagating mode is what the cross-section actually carries at the frequency being evaluated; their number grows along the axis, each new one starting at its cut-on. Modes are matched to channels per frequency by a weighted overlap test, and nothing guarantees the two counts agree.

They can disagree in either direction, and the two cases behave oppositely. A channel without a mode is visible: it finds no overlap, takes no assignment, its S stays NaN at that frequency, and the channels that were matched are unaffected (measured at \(10^{-15}\) relative, roundoff). This is the ordinary evanescent case — declaring more channels than the axis excites costs a NaN column and nothing else. A mode without a channel is not visible: nothing is skipped, so nothing goes NaN, and the fit attributes the missing mode’s recorded content to the modes that remain. The result is a biased S-parameter that looks like a converged one. Measured on a two-family fixture whose port was truncated to n_modes = 1 above its second cut-on at 8.4465 GHz, \(S_{11}\) came out 23 to 30 % wrong, with the contamination at \(-129\) dB when the unaccounted mode carried the same amplitude as the fundamental and rising exactly 20 dB per decade of that amplitude ratio. Read that level as the mechanism and its scaling law, not as a bound: its coefficient is the biorthogonality defect between one port’s recording profiles and its true modes (about \(6\cdot10^{-7}\) on that cross-section), and a port whose dual basis is less clean sits correspondingly higher.

The rule for a band run follows: n_modes must cover every mode that propagates anywhere on the measurement axis — which is not the same as every mode of interest, and not the same as the tracked band — or the axis must stay below the next cut-on of the cross-section. Magnelio counts the modes it finds against the modes it assigns at each axis point and warns once per port, naming how many frequencies are affected and their span. The warning does not repair the bias: the port genuinely carries fewer channels than its cross-section carries modes, and only a rebuilt port or a shorter axis fixes that. What it does is keep a model error from reading as a result.

Reference-plane shift (de-embedding, DD-187)#

result.deembed({"port1": d}) returns the S-matrix referenced at planes shifted a distance \(d\) from the port planes into the domain (negative distances move outward): every S-parameter touching a shifted port is multiplied by the inverse line propagation factor over \(d\) — reflections twice, transmissions once per shifted end. This is the classical reference-plane transformation [16]; the in-house refinement is which propagation factor is removed.

Wherever the run certified a channel’s discrete line parameters, the shift uses the exact discrete dispersion of the feed chain — the same characteristic root \(\lambda(z)\) the transparent boundary is built from — evaluated on the unit circle, so passband magnitudes are untouched exactly and the removed phase is exactly the phase the grid applied. De-embedding a uniform feed line therefore cancels it to the accuracy floor of the run itself (measured: −120 dB TEM, −67 dB TE10 at 8 cells/λ), whereas the textbook continuum \(e^{-\gamma d}\) would leave the grid-dispersion gap behind — degrees of phase on coarse meshes, silently attributed to the device under test. For a quasi-TEM channel — microstrip, CPW, any inhomogeneous cross-section, terminated by modal Mur in the default pipeline — there are no certified line parameters, and the shift uses the port’s dispersion record instead: the true discrete modes of the feed cross-section, solved at every point of the axis (the same solve behind report.dispersion), so the removed propagation is the one the grid applied, physical dispersion included. On the 20 mm tutorial microstrip (0.8 mm \(\varepsilon_r = 4.3\), 25 nodes per wavelength) the S21 phase left after de-embedding the whole line is 0.3°, 1.5° and 1.8° at 5, 10 and 15 GHz, against \(-1.5\)°, \(-11\)° and \(-30\)° with the quasi-static \(\gamma\) the fallback used to take; what remains is the drive port’s launch residue (DD-239), not the line. Only a channel with neither — a feed section that is not a uniform chain behind the port — falls back to the mode’s continuum \(\gamma(\omega)\), for a quasi-TEM mode the frequency-flat quasi-static one, which leaves the line’s dispersion in the de-embedded matrix.

The shift assumes the port cross-section continues over the shifted length. Below its cut-off a channel’s factor grows as \(e^{+\alpha d}\); those bins keep the diagnostic character the raw values have. Lumped ports carry no feed line and cannot be de-embedded.

Reference impedance, dispersion and renormalisation (DD-244)#

Every S-parameter is a ratio of power waves, and power waves are defined against a reference impedance. Magnelio measures each channel against its own: the impedance its port mode carries on the grid — result.reference_impedance(port) on any scattering result. On a TEM or quasi-TEM channel that is the line impedance the port report prints (frequency-flat; the quasi-static value on an inhomogeneous line), on a certified Klein–Gordon channel the exact discrete wave impedance of the chain, on a hollow-pipe mode the wave impedance, which varies with frequency, on a lumped port its Thévenin impedance. A uniform line is therefore matched in the raw result whatever its impedance came out at: tutorial 02’s coax reflects \(-110\) dB while its grid impedance sits a few percent off the design value, because port matching is between port and grid.

What the modes really carry. report.dispersion(f_axis) on a port report solves the true discrete modes of the feed cross-section at every requested frequency — the ζ-pencil above, continued along the axis from each mode’s previous eigenvalue so a 201-point sweep costs a few seconds — and returns per mode the impedance, the effective permittivity and the propagation constant \(\gamma = \alpha + j\beta\) as functions of frequency. The impedance is the power–current definition \(Z_{PI} = 2P/|I|^2\) of the true mode, from the discrete Poynting flux through the port plane and the Ampère loop around the signal conductor; on a homogeneous line it equals the report’s line impedance to roundoff, on the tutorial microstrip it meets the quasi-static value in the static limit and rises to 47.8 Ω at 15 GHz from 46.0 Ω, while \(\varepsilon_{\text{eff}}\) walks from 3.05 to 3.37 — the curve the S21 phase of the full run reproduces. On a port with more than one signal conductor the modal current has no single loop and the sweep reports the channel’s own reference instead. Hollow pipes work the same way (the TE/TM wave impedance, frequency by frequency); lumped ports carry no cross-section and raise.

Renormalisation. result.renormalize(50) returns the same network re-referenced to a common impedance — what a network analyser with 50 Ω reference planes reads, and what a circuit simulator needs before it cascades this block with others. The transformation is the exact power-wave re-referencing for real references [27]: with \(\rho_i = (Z_i - Z'_i)/(Z_i + Z'_i)\) and \(c_i = (Z_i + Z'_i)/(2\sqrt{Z_i Z'_i})\),

\[ S' = C\,(S + \rho)\,(I + \rho S)^{-1}\,C^{-1}, \]

per frequency, on the complete matrix (every observed channel excited — a channel’s own reflection enters every other entry once its reference moves). A dict renormalises port by port, an array per frequency. Whether the mismatch that appears is real is a modelling question: a 49 Ω grid line feeding a 50 Ω system does reflect, and a line meant to be the 50 Ω one shows a discretisation artefact — which is where the next subsection comes in.

Exports. A Touchstone file states one constant reference impedance for all ports in its option line, so to_touchstone writes that value when every exported channel shares one frequency-flat reference (the 49.3 Ω of a coax as it came out, not a nominal 50), and takes z_ref=50 to renormalise first. Channels whose references differ or vary with frequency cannot be expressed in Touchstone 1.x; the file is then written with a nominal R 50, a warning, and each port’s actual reference in the header, and a reader that renormalises on R would be wrong. to_skrf carries per-port, per-frequency references in the network’s z0 and needs no such choice.

Converging the port cross-section (DD-244)#

The 2D mode problem is the port-plane slice of the 3D operator, so its parameters carry the discretisation of the user’s mesh, and converging them by refining that mesh costs \(8\times\) cells per halving. refine_port_modes(model, control, mesh, "port1") converges them on a port slab instead — by default the line impedance of a TEM or quasi-TEM mode and the cut-off frequency of a TE or TM mode, which has no line impedance (target= names one of z_line, epsilon_eff, f_cutoff explicitly): a slice of the model a few cells deep behind the port face, cut with the mesher’s own domain clip, meshed with the user’s control at level 0 — which reproduces the user’s port-plane grid and its report exactly — and with every tangential cell split \(2^k\) ways at level \(k\) (MeshControl(subdivide=...), a nested h-refinement of the finished grid that keeps every plane the rules placed). Each rung costs \(4\times\) the previous one on a slab of six cells, the report lists the ladder, its observed order and a Richardson estimate, and the lesson is the one tutorial 09 draws: its 25-node-per-wavelength grid reads a microstrip designed on paper for 50 Ω at 46.0 Ω, while the ladder converges at first order (the strip edges) toward about 51.5 Ω — an 11 % grid error that neither the port’s matching nor the run’s \(|S_{11}|\) shows, and exactly the number a renormalisation to 50 Ω would attribute to the device; the remaining 3 % against the paper design splits into the shield (about 1.2 Ω, the ladder rerun in a four times larger box converges at 52.7 Ω) and the hand formula’s thickness correction.

What a Touchstone export covers (DD-184)#

to_touchstone() and to_skrf() export the square sub-matrix over the excited channels: one Touchstone port per channel, so a multi-mode port occupies one port per mode. Channels that were observed but not excited are dropped from both the rows and the columns; nothing is padded.

Dropping them is sound because an unexcited channel is not an open circuit. Every port carries its own reflection-free boundary throughout the run, so the omitted channels are matched — which is exactly the termination condition the definition of S-parameters asks for. The export is the network seen with those channels terminated, the same quantity a vector network analyser measures with its unused ports on 50 Ω loads. Exciting one port of a two-port and writing the reflection as a .s1p is therefore a valid export, not a truncated one.

What the reduction does not carry is mode conversion at a port that is itself exported. If port1 is solved with three modes, only mode 0 is excited, and modes 1 and 2 propagate inside the exported band, then power scattered from mode 0 into those modes leaves the matrix: the file looks like a complete two-port while the component is not one. Such an export warns, naming the port and the cut-off above which the omitted modes propagate. Evanescent omitted modes draw no warning — solving for more modes than one excites, so that the evanescent content is represented at the port plane, is ordinary practice.

Pass channels= to select the sub-network explicitly, e.g. channels=["port1", "port3"] to cut a two-port out of a fully excited three-port.

The .sNp extension must agree with the number of exported channels. Touchstone 1.x records the port count nowhere else — the file body has no field for it — so a .s6p holding two-port rows is not merely misnamed but unreadable; a mismatch raises rather than writes. A path given without an extension gets the matching one, so to_touchstone("wr90") writes wr90.s2p.