magnelio.post#

Post-processing components — S-parameter computation helpers. The result objects themselves come back from the analyses; this component holds the free functions and containers for custom pipelines.

class magnelio.post.FarFieldResult(f, theta, phi, E_theta, E_phi, accepted_power=None, physical_mask=None, physical_sphere_fraction=1.0)#

Far-field pattern of one frequency.

The complex patterns are the r-independent far-zone amplitudes: the physical field at distance r is E(r) = (E_theta θ̂ + E_phi φ̂) · e^{-jkr} / r. Like every frequency-domain field of the library they are effective (RMS) phasors per √W of incident CW power, which makes realized_gain the directly measured quantity and keeps the intensity free of a peak-phasor ½.

Parameters:
f#

Frequency [Hz].

Type:

float

theta, phi

Evaluation angles [rad]; θ from +z (ISO convention), φ from +x.

Type:

ndarray

E_theta, E_phi

Far-zone amplitudes [V].

Type:

(n_theta, n_phi) complex ndarray

accepted_power#

Accepted input power [W] behind gain; None until wired by the analysis.

Type:

float or None

physical_mask#

False in directions behind an infinite ground plane; None when the whole sphere is physical.

Type:

(n_theta, n_phi) bool ndarray or None

physical_sphere_fraction#

Solid-angle share of the physical region (0.5 per ground plane). The image expansion makes the pattern mirror-symmetric about every such plane, so the physical-region power integral is exactly this fraction of the smooth full-sphere integral — which avoids the half-cell quadrature bias a hard mask edge would cost.

Type:

float

cut(*, plane='phi', angle=0.0, quantity='realized_gain')#

One pattern cut for polar plotting.

Parameters:
  • plane ({"phi", "theta"}) – "phi" fixes the azimuth: the cut runs over θ at the azimuth angle (and continues over the back half at angle + π, giving the full 0…2π polar trace). "theta" fixes the polar angle and runs over φ.

  • angle (float) – The fixed angle [rad].

  • quantity (str) – "realized_gain" (default), "gain", "directivity" or "U".

Returns:

angles, values – Cut angles [rad] and the (linear) quantity along the cut.

Return type:

ndarray

plot_3d(*, quantity='realized_gain', **kwargs)#

3D radiation surface of the pattern.

Extra keyword arguments go to magnelio.plots.plot_pattern_3d() (db=, floor_db=, ax=, cmap=, title=).

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (mpl_toolkits.mplot3d.axes3d.Axes3D)

Parameters:

quantity (str)

plot_cut(*, plane='phi', angle=0.0, quantity='realized_gain', **kwargs)#

Polar plot of one pattern cut (see cut()).

Extra keyword arguments go to magnelio.plots.plot_pattern_cut() (db=, floor_db=, ax=, label=, title=).

Returns:

  • fig (matplotlib.figure.Figure)

  • ax (matplotlib.projections.polar.PolarAxes)

Parameters:
  • plane (str)

  • angle (float)

  • quantity (str)

property P_rad: float#

∮ U dΩ over the physical sphere.

Type:

Total radiated power [W]

property U: ndarray#

Radiation intensity [W/sr], zero behind a ground plane.

property directivity: ndarray#

Directivity 4π U / P_rad (linear).

property gain: ndarray#

IEEE gain 4π U / P_accepted (linear).

property radiation_efficiency: float#

P_rad / P_accepted — 1.0 for a lossless model.

property realized_gain: ndarray#

Realized gain 4π U / P_incident (linear).

The pattern of a solver run is per √W of incident power, so the reference is exactly 1 W and mismatch loss is included.

class magnelio.post.SParameterResult(f_axis, channels, excitations, matrix)#

Structured multi-port S-parameter spectrum.

A single SParameterResult holds the full S-matrix S[f, observed, excited] of a multi-port network at a fixed list of frequencies, along with the channel labels and the subset of channels that were actually excited.

Channels are addressed by (port_name, mode_idx) tuples; the convenience accessors (S(), db()) take the more ergonomic port_name strings plus optional mode indices.

Parameters:
  • f_axis (ndarray)

  • channels (tuple[tuple[str, int], ...])

  • excitations (tuple[tuple[str, int], ...])

  • matrix (ndarray)

f_axis#

Real, strictly positive frequencies [Hz], shape (Nf,).

Type:

np.ndarray

channels#

Observed channels in canonical order. Same ordering as the rows of matrix.

Type:

tuple of (str, int)

excitations#

Excited channels in canonical order — a subset of channels. Same ordering as the columns of matrix.

Type:

tuple of (str, int)

matrix#

Complex S-matrix. matrix[k, i, j] is S(observed=i, excited=j) at frequency f_axis[k].

Type:

np.ndarray, shape (Nf, n_channels, n_excitations)

Notes

The wrapper enforces channel-key consistency at construction; the matrix shape must match (Nf, len(channels), len(excitations)).

Single-excitation runs produce an SParameterResult with n_excitations = 1; that is the natural form for one Phase-2 FIT-TD simulation result. Use merge() to combine K such results into the full K-excitation S-matrix.

classmethod from_multiple_excitations(runs, f_axis, *, channel_order=None)#

Aggregate K single-excitation runs into a multi-column result.

Each runs[k] = (excited_k, s_dict_k) contributes one column of the S-matrix at column index k. All s_dict_k must share the same observed-channel set (otherwise the resulting matrix would have NaN holes — refuse rather than fill).

Parameters:
  • runs (list of ((str, int), dict[(str, int), np.ndarray])) – List of (excited, s_dict) pairs, in the order to assemble the columns. All s_dicts must share the same key set.

  • f_axis (np.ndarray) – Frequency axis common to all runs, shape (Nf,).

  • channel_order (tuple of (str, int), optional) – Override the canonical observed-channel ordering. Must enumerate every key shared by all s_dicts. Default: keys of the first s_dict in iteration order.

Return type:

SParameterResult

classmethod from_single_excitation(s_dict, excited, f_axis, *, channel_order=None)#

Wrap a single-column compute_s_parameters() dict.

Parameters:
  • s_dict (dict[(str, int), np.ndarray]) – Direct output of compute_s_parameters(). Each value is a frequency-domain S-parameter spectrum of shape (Nf,).

  • excited ((str, int)) – (port_name, mode_idx) of the source for this run. Must be a key of s_dict.

  • f_axis (np.ndarray) – Frequency axis the spectra were sampled at, shape (Nf,).

  • channel_order (tuple of (str, int), optional) – Override the canonical ordering of observed channels. Default: keys of s_dict in their dict-iteration order (insertion-order in CPython ≥ 3.7).

Return type:

SParameterResult

classmethod merge(results)#

Combine K single-excitation SParameterResult objects.

All inputs must share the same f_axis and the same channels ordering. Each input’s single excitation becomes one column of the merged matrix; column order follows the input list.

Parameters:

results (list[SParameterResult])

Return type:

SParameterResult

S(out_port, in_port, *, mode_out=0, mode_in=0)#

Return the spectrum S(out_port, in_port).

Conventionally S_{out,in} is the wave amplitude received at the out port-mode in response to a unit-amplitude wave injected at the in port-mode.

Parameters:
  • out_port (str) – Port labels. Must match channel keys.

  • in_port (str) – Port labels. Must match channel keys.

  • mode_out (int, optional) – Mode indices on each port (default 0).

  • mode_in (int, optional) – Mode indices on each port (default 0).

Return type:

ndarray

channel_index(port, mode=0)#

Index of (port, mode) into channels.

Parameters:
  • port (str)

  • mode (int)

Return type:

int

db(out_port, in_port, *, mode_out=0, mode_in=0, floor_db=-200.0)#

Return 20·log10|S(out, in)| with a floor at floor_db.

The floor avoids -inf at frequencies where the S-parameter is at the FFT round-off (|S| 1e-12 or smaller). Default -200 dB is below any physically meaningful response.

Parameters:
  • out_port (str)

  • in_port (str)

  • mode_out (int)

  • mode_in (int)

  • floor_db (float)

Return type:

ndarray

excitation_index(port, mode=0)#

Index of (port, mode) into excitations.

Parameters:
  • port (str)

  • mode (int)

Return type:

int

to_skrf(name='magnelio')#

Return the complete S-matrix as a skrf.Network.

Requires scikit-rf (install extra magnelio[interop]) and a complete square matrix; multi-mode ports map to one network port per channel, in canonical channel order.

Return type:

skrf.Network

Parameters:

name (str)

to_touchstone(path)#

Write the complete S-matrix as a Touchstone .sNp file.

Requires every observed channel to have been excited (a square matrix); raises otherwise. Touchstone ports are the channels in canonical order — a multi-mode port occupies one Touchstone port per mode; the mapping is recorded in the file’s comment header. Data are the power-wave (generalised) S-parameters on the per-mode reference impedances; the nominal R 50 of the option line does not renormalise them.

Parameters:

path (str or pathlib.Path) – Output file; conventionally <name>.s{N}p.

Return type:

None

property is_complete: bool#

True iff every observed channel was also excited (K×K matrix).

property port_names: tuple[str, ...]#

Unique port names in first-occurrence order across channels.

magnelio.post.compute_band_s_parameters(recorder_signals, ports, excited, f_axis, *, a_threshold=1e-12)#

S-parameters of a pulsed broadband run through band DTBC ports.

The per-frequency true-mode decomposition applied to a single pulsed record: per f_axis point the true discrete modes of each port cross-section are solved on the stored inward chain, their exact V/I responses through the port’s fixed recording profiles are synthesised (cw_wave_phasors() — the de-stagger and the discrete wave impedance are contained in the phasors), and the recorded spectra are decomposed by the joint linear system

V_c(f) = sum_j a_j v_in[c, j] + b_j v_out[c, j] I_c(f) = sum_j a_j i_in[c, j] + b_j i_out[c, j]

over all recording channels c and all modes j propagating at f. S[(port, c)] = b_(port, c) / a_excited.

Cost note: each frequency point runs one sparse mode solve per port (measured 30 ms – 3.6 s depending on cross-section size) plus one curl application per (channel, mode) pair — independent of the 3D run, which is needed only once for the whole axis.

Parameters:
  • recorder_signals (dict) – Output of PortSignalRecorder.finalize(), keyed by (port_label, mode_idx).

  • ports (list) – The PortOperatorBandDTBC instances of the run (each must carry band_data).

  • excited ((str, int)) – (port_label, channel) of the pulsed source.

  • f_axis (np.ndarray) – Evaluation frequencies [Hz]; must lie inside every port’s subspace band (content outside the band is not certified).

  • a_threshold (float, default 1e-12) – Relative |a_excited| floor below which S is NaN.

Returns:

Complex S spectrum per (port, channel) against the excited channel. Channels whose family does not propagate at a frequency carry NaN there.

Return type:

dict[(str, int), np.ndarray]

magnelio.post.compute_s_parameters(recorder_signals, port_modes, excited, reference_signal, f_axis, *, a_threshold=1e-12, taper_signals=False, port_normal_dx=None, port_line_params=None)#

Compute power-wave S-parameters from recorded V/I time-series.

Parameters:
  • recorder_signals (dict) – Output of PortSignalRecorder.finalize(). Keys are (port_label, mode_idx); values are (V_signal, I_signal). Both signals share the same naive time axis t = arange(N)·dt; the half-step Yee stagger between V and I is corrected internally by this function.

  • port_modes (dict[str, list[Mode]]) – Per-port ordered list of Mode objects. Mode index in each list must align with the recorder’s mode_idx. Used to evaluate the modal reference impedance Z(ω) at every f_axis point.

  • excited ((str, int)) – (port_label, mode_idx) of the source. Defines the denominator a_excited(f) of the S-parameter ratio.

  • reference_signal (Signal1D) – Original user excitation waveform s(t). Currently unused in the S-parameter math itself (a_excited is extracted from V/I for self-consistency); kept on the signature as a sanity-check anchor and so the API does not need to change when PortOperatorModal adopts the §4.3 power-normalised injection.

  • f_axis (np.ndarray) – Target frequencies [Hz], shape (Nf,). Must be > 0.

  • a_threshold (float, default 1e-12) – Relative threshold below which |a_excited(f)| / max(|a_excited|) is treated as numerical floor and S is reported as NaN.

  • taper_signals (bool, default False) – If True, multiply every recorder V and I array with a symmetric Tukey window of alpha = 0.05 (i.e. the first and last 2.5 % of samples taper to zero, the inner 95 % stay untouched) before the DFT. Suppresses the rectangular-window sidelobes caused by a non-vanishing residual at the truncation edge — useful when energy_stop_db terminates the run while the tail of the transient is still ringing above the FFT noise floor. Off by default; the rectangular window is the unbiased reference for closed-form mode validation.

  • port_normal_dx (dict[str, float] or None, default None) –

    Per-port boundary-cell size along the port normal (PortOperatorModal.plane.normal_dx), keyed by port name. When given for a port, the spatial half-cell stagger of the I sampling plane is compensated exactly (concern 1b above) for every channel of that port. Ports missing from the dict (and the None default) fall back to the co-located (V Z·I)/2 decomposition — correct for lumped ports, where V and I live on the same cell, and the historical behaviour for modal ports.

    Residual accuracy after de-stagger: the historical grading floor (measured: growth-1.4 transversal grading at ≈ −23 dB) was a V/I measurement error of the pointwise H-voltage convention and is resolved by the travelling-wave port profiles — graded transversal port meshes are first-class. For TEM modes, the pair (exact DTBC termination + the port_line_params discrete de-stagger below) removes the remaining absorber and measurement floors entirely (straight-line benchmarks −131 … −159 dB). TE/TM modes on certified uniform chains run the Klein-Gordon DTBC, which removed the former Mur-1st ≈ −19 dB near-cutoff peak structurally; only analytical-path modes remain on Mur-1st.

  • port_line_params (dict[(str, int), tuple] or None, default None) – Per-channel discrete line parameters (r, q) or (r, q, z0) — the modal Courant number, discrete cut-off × dt, and static impedance constant of the exact 1D chain certified by the port termination (PortOperatorModal.dtbc_line_params). Channels present here (and whose port is in port_normal_dx) are de-staggered with the exact discrete half-cell factor λ^{1/2} instead of the continuum e^{−γ·d/2} (concern 1c above). Channels carrying a non-None z0 (certified Klein-Gordon chains) additionally replace the continuum z_modal(ω) by the exact discrete wave impedance dtbc_wave_impedance() — the continuum impedance misses the discrete wave’s V/I by O((ω·dt)², (β·dz)²), a −40…−60 dB measured-|S11| cap on λ/20 meshes. Missing channels fall back to the continuum forms.

Returns:

Mapping (port_label, mode_idx) S(f_axis) of shape (Nf,), complex. One entry per channel in recorder_signals.

Return type:

dict[(str, int), np.ndarray]

Raises:
  • KeyError – If excited is not a key in recorder_signals, or if a recorder channel references a port not present in port_modes.

  • ValueError – If f_axis contains a non-positive frequency, or if a mode index is out of range for its port.

Notes

Degenerate mode pairs. Two modes with identical cut-offs (coax TE11 polarisations, TE_mn/TM_mn in a rectangle) span a degenerate subspace: the individual per-channel S_ij entries within the pair depend on the (mesh-dependent) basis the eigensolver picked, and the cross-coupling between the partners does not vanish with refinement — build_modal_port warns. The basis-independent transmission observable is the total power into the degenerate subspace, Σ_k |S(out_k, in)|² summed over the pair — the convention the examples/straight_waveguide_*.py acceptance scripts print. Reflection S_mm of the excited channel itself is basis-independent for a symmetric discretisation.

magnelio.post.ntff_transform(patches, image_planes, f, theta=None, phi=None, accepted_power=None)#

Far-field pattern from tangential surface fields at one frequency.

Parameters:
  • patches (sequence of SurfacePatchSet) – The Huygens surface, in as many pieces as convenient.

  • image_planes (sequence of ImagePlane) – Boundary planes the surface does not cross; each doubles the patch set with its mirror image.

  • f (float) – Frequency [Hz].

  • theta (array_like, optional) – Spherical evaluation angles [rad]; θ from the +z axis, φ from the +x axis in the xy-plane. Default: 2° grids over the full sphere.

  • phi (array_like, optional) – Spherical evaluation angles [rad]; θ from the +z axis, φ from the +x axis in the xy-plane. Default: 2° grids over the full sphere.

  • accepted_power (float, optional) – Accepted input power [W] for the gain normalisation; usually wired by the analysis.

Return type:

FarFieldResult

magnelio.post.wall_loss_Q(result, mode=0, *, sigma=None, mu=1.0, roughness=None)#

Wall-loss Q factor of an eigenmode (perturbative power-loss).

Parameters:
  • result (EigenmodeResult) – A lossless eigenmode solution (fields + mesh). PEC domain walls are read from solver_info['boundary_conditions'] (omitted faces default to PEC, the solver convention).

  • mode (int) – Mode index into result.modes.

  • sigma (float, optional) – Conductivity [S/m] for walls that are not lossy metals (plain-PEC solids and PEC boundary walls). Lossy-metal solids always use their own material values.

  • mu (float, optional) – Relative permeability accompanying sigma (default 1).

  • roughness (SurfaceRoughness, optional) – Surface-roughness model for the same walls sigma applies to; lossy-metal solids always use their own. None (default) is a perfectly smooth conductor.

Return type:

WallLossQ