Skip to content

Working with Results

This page describes how to work with simulation results in Flexcompute RF (also Flex RF).

A successful simulation run returns a result data object whose type depends on the submitted simulation type.

Submitted simulationResult data object
TerminalComponentModelerTerminalComponentModelerData (TCMData)
ModeSolverMicrowaveModeSolverData

The TCMData is the top-level container for component modeler results. It bundles together the port S-matrix, antenna metrics, and the individual per-port SimulationData objects, which in turn hold the raw monitor data from each port-excitation run.

# Run a TerminalComponentModeler and get overall TCM results
tcm_data = web.run(my_tcm, task_name='my tcm job')
# Top-level methods on TCMData
s_matrix = tcm_data.smatrix() # S-parameters
antenna_data = tcm_data.get_antenna_metrics_data() # Antenna metrics
# Per-port SimulationData (one entry per excited port-mode combo)
sim_data_port1 = tcm_data.data['port_1']
# Individual monitor data from a SimulationData
field_data = sim_data_port1['my field monitor']

When submitting a list or dict of simulations (a sweep), the returned result mirrors the input structure. Each entry is the result data object corresponding to the simulation at that key.

# Sweep results are accessed using the same keys as the input batch
batch_results = web.run({'config_A': tcm_A, 'config_B': tcm_B}, task_name='my sweep')
tcm_data_A = batch_results['config_A']

Result data in Flex RF is built on top of xarray, which provides labeled multi-dimensional arrays. Each data array carries named coordinates (e.g. f, port_in, port_out, theta, phi), so that data can be selected by label rather than by integer index.

Use .sel() for label-based selection and .isel() for positional indexing.

# Select S11 across all frequencies
s11 = s_matrix.data.sel(port_out='port_1', port_in='port_1')
# Select gain at a specific frequency (nearest match)
g = antenna_data.gain.sel(f=10e9, method='nearest')
# Select gain at a single (theta, phi) direction
g_broadside = antenna_data.gain.sel(theta=0, phi=0, method='nearest').squeeze()

Xarray data can be converted to NumPy with .values, or to a pandas DataFrame with .to_dataframe() for convenient CSV export. Data can also be saved to disk in NetCDF or HDF5 formats using .to_netcdf() or .to_hdf5().

# Convert to NumPy
s11_array = s11.values
# Export to file
s_matrix.data.to_netcdf('s_matrix.nc')

Port indices are used when indexing into TerminalComponentModelerData to retrieve per-port SimulationData, as well as retrieving a column/row in the S-matrix.

For LumpedPort, the port index is simply the port name attribute.

# Define lumped port 'LP1'
my_lumped_port = LumpedPort(name='LP1', ...)
# Access data associated with LP1
sim_data = tcm_data.data['LP1']
S11 = tcm_data.smatrix().data.sel(port_in='LP1', port_out='LP1')

For WavePort, the port index is in the format <name>@<mode_num> where name is the port name and mode_num is the n-th mode (zero-indexed).

# Define wave port solving for 2 modes
my_wave_port = WavePort(
name='WP1',
mode_spec=MicrowaveModeSpec(num_modes=2, ...),
...
)
# Access data associated with WP1 and mode 0
sim_data = tcm_data.data['WP1@0']
S11 = tcm_data.smatrix().data.sel(port_in='WP1@0', port_out='WP1@0')

For TerminalWavePort, the port index is in the format <name>@<terminal_label>, where terminal_label identifies which single-ended terminal or differential mode is being driven/observed. Single-ended terminals are labeled T0, T1, …, Tn (from left-to-right, bottom-to-top by default). Differential pairs are labeled Diff0@comm, Diff0@diff, Diff1@comm, … (ordered by their entry in differential_pairs), where @comm and @diff denote the common and differential modes of the pair. The active single-ended terminals come first in the matrix ordering, followed by the differential pairs.

# TerminalWavePort with 3 single-ended terminals (T0, T1, T2)
# (T0, T1) are grouped as a differential pair
my_terminal_wave_port = TerminalWavePort(
name='TWP1',
differential_pairs=[('T0', 'T1')],
...
)
# After grouping, the active port indices for TWP1 are:
# 'TWP1@T2' — remaining single-ended terminal
# 'TWP1@Diff0@comm' — common mode of pair 0
# 'TWP1@Diff0@diff' — differential mode of pair 0
# Access differential-mode data on TWP1
sim_data = tcm_data.data['TWP1@Diff0@diff']
S_diff = tcm_data.smatrix().data.sel(
port_in='TWP1@Diff0@diff', port_out='TWP1@Diff0@diff'
)

Use TerminalComponentModeler.plot_port() to visually inspect the resolved terminal labels for a given port.

The smatrix() method on TCMData returns a MicrowaveSMatrixData object containing the computed S-parameters.

# Compute the S-matrix
s_matrix_data = tcm_data.smatrix()
# Underlying S-matrix array, indexed by (f, port_out, port_in)
s_array = s_matrix_data.data
# Select a specific S-parameter, e.g. S21
s21 = s_array.sel(port_out='port_2', port_in='port_1')
s21_dB = 20 * np.log10(np.abs(s21))

The S-matrix can also be converted to a Z-matrix using the port reference impedances via s_to_z().

# Convert S-matrix to Z-matrix
z_matrix = tcm_data.s_to_z()
z11 = z_matrix.sel(port_out='port_1', port_in='port_1')

To access the raw port voltages and currents at the simulation reference plane, use the port_voltage_current_matrices property. The returned arrays have dimensions (f, port_out, port_in), where port_in indexes the excited port and port_out indexes the observation port.

# Raw port voltage and current matrices
v_matrix, i_matrix = tcm_data.port_voltage_current_matrices

The port reference impedance itself can be queried via port_reference_impedances. For LumpedPort and WavePort, this is a diagonal matrix. For TerminalWavePort, the matrix may have off-diagonal coupling between the terminal modes.

# Port reference impedance matrix
z_ref = tcm_data.port_reference_impedances

Use renormalize() to recompute the S-matrix against a different reference impedance, without rerunning the simulation. This returns a new TCMData with the new reference impedance applied; calling smatrix() on the new object returns the renormalized S-parameters.

# Renormalize all ports to a uniform 75 Ohm reference
tcm_data_75 = tcm_data.renormalize(75)
s_matrix_75 = tcm_data_75.smatrix()

The reference_impedance argument also accepts a per-port PortDataArray or a full TerminalPortDataArray for frequency- or port-dependent renormalization.

For wave-port simulations, the S-parameter reference plane can be shifted by a desired offset using smatrix_deembedded(). The shift is specified per port as a length in microns; a positive value shifts the reference plane in the direction of the port’s outward normal.

# Shift the reference plane of WP1 inward by 100 um
port_shifts = PortNameDataArray([-100.0], coords={'port': ['WP1']})
s_matrix_deembed = tcm_data.smatrix_deembedded(port_shifts=port_shifts)

De-embedding is currently only supported for WavePort and TerminalWavePort, since it relies on the port mode propagation constant to compute the phase shift.

Each entry of tcm_data.data is a SimulationData corresponding to the simulation run where a single port was excited. Individual monitor data are accessed by the monitor name.

# SimulationData for the run that excited 'port_1'
sim_data = tcm_data.data['port_1']
# Access data of a specific monitor by name
field_data = sim_data['my field monitor']
# Access the underlying field components, indexed by (x, y, z, f)
Ex = field_data.Ex
Ez_at_f0 = field_data.Ez.sel(f=f0, method='nearest')

To quickly visualize a field monitor with the simulation geometry overlaid, use the plot_field() method of SimulationData.

# Plot |Ez| at frequency f0 from a field monitor named 'my field monitor'
sim_data.plot_field(
field_monitor_name='my field monitor',
field_name='Ez',
val='abs', # 'real', 'imag', 'abs', 'abs^2', or 'phase'
f=f0,
ax=ax,
)

Use field_name='E' or field_name='H' to plot the field vector magnitude, or field_name='S' for the Poynting vector. For 3D monitors, pass a slice coordinate (e.g. z=0) to specify the cut plane.

The SurfaceFieldMonitor records the normal E-field and tangential H-field on PEC and lossy metal surfaces. Its result data (SurfaceFieldData) is stored with fields evaluated at triangulated surface points.

# Access surface field data
surf_data = sim_data['my surface monitor']
# Underlying field datasets
E_surface = surf_data.E
H_surface = surf_data.H
# Surface current density
J_surface = surf_data.current_density
# Time-averaged Poynting vector on the surface
poynting = surf_data.poynting

The triangulated surface fields are rendered with PyVista. Use .plot() for a scalar surface map, or .quiver() to draw the vector field as 3D arrows. The dataset must be reduced to a single scalar (for .plot()) or to a single 3-component vector field (for .quiver()) before plotting — select along the f and (where applicable) side dimensions first.

# Plot the surface current density magnitude |J_s| at f0
J_at_f0 = surf_data.current_density.sel(f=f0, method='nearest')
J_at_f0.norm(dim='axis').plot(cmap='inferno')
# Plot the surface current density as vector arrows at f0
J_at_f0.quiver(scale=0.1, color='magnitude', cmap='inferno')

The same pattern works for surf_data.E and surf_data.H, except they additionally carry a side dimension ('inside' / 'outside') that must also be selected before plotting.

# Plot |H| on the outside of the surface at f0
H_at_f0 = surf_data.H.sel(f=f0, method='nearest', side='outside')
H_at_f0.norm(dim='axis').plot()

Antenna metrics are computed via get_antenna_metrics_data() on the TCMData, which returns an AntennaMetricsData object. The method projects the near-field recorded by a DirectivityMonitor into the far-field, and computes common antenna quantities.

# Compute antenna metrics from the first radiation monitor and first excited port
antenna_data = tcm_data.get_antenna_metrics_data()

The returned object exposes the following commonly used attributes, all indexed by (f, theta, phi) for spatial metrics, or (f) for aggregate metrics:

  • directivity, gain, realized_gain — antenna gain and directivity
  • radiation_intensity — radiation intensity in W/sr
  • axial_ratio — polarization axial ratio
  • power_incident, power_reflected, supplied_power — power balance
  • radiation_efficiency, reflection_efficiency — antenna efficiencies
  • Etheta, Ephi, Er — far-field E-field components

The polarization-resolved gain and directivity are obtained from partial_gain() and partial_directivity(). Both methods accept pol_basis='linear' (default, returns the θ\theta- and ϕ\phi-polarized components) or pol_basis='circular' (returns RHCP and LHCP components).

# Linear polarization decomposition
pg_linear = antenna_data.partial_gain(pol_basis='linear')
g_theta = pg_linear.Gtheta
g_phi = pg_linear.Gphi
# Circular polarization decomposition
pg_circ = antenna_data.partial_gain(pol_basis='circular')
g_RHCP = pg_circ.Gright
g_LHCP = pg_circ.Gleft

By default, get_antenna_metrics_data() uses only the first port without any scaling of the raw simulation data. To compute the antenna metrics from a superposition of port excitations (e.g. for phased-array beamforming or differential excitation), pass a port_amplitudes dictionary that maps each port (or (port, mode_index) pair for wave ports) to a complex amplitude. The amplitude follows the power-wave convention, where 0.5a20.5 |a|^2 is the incident power into the port.

# Differential excitation: port_1 driven with amplitude 1, port_2 with -1
port_amplitudes = {'port_1': 1.0, 'port_2': -1.0}
antenna_diff = tcm_data.get_antenna_metrics_data(port_amplitudes=port_amplitudes)

For an N-element phased array, build the amplitude dictionary programmatically with the desired magnitude and phase per element.

# Uniform-amplitude phased array steered to a beam direction
N = 8
beam_phase = np.pi / 4 # progressive phase shift between elements
port_amplitudes = {
f'port_{i+1}': np.exp(1j * beam_phase * i) for i in range(N)
}
antenna_steered = tcm_data.get_antenna_metrics_data(port_amplitudes=port_amplitudes)

If multiple radiation monitors are defined, select which one to use with the monitor_name argument.

The LobeMeasurer extracts standard radiation pattern metrics — main lobe direction, half-power beamwidth (HPBW), first-null beamwidth (FNBW), and side-lobe level — from a 1D radiation pattern.

# Extract a 1D pattern along phi=0 cut
theta = np.linspace(0, 2 * np.pi, 361)
pattern = antenna_data.gain.sel(phi=0, f=f0, method='nearest').squeeze().values
# Construct the lobe measurer (pattern must be linear scale, not dB)
lobes = LobeMeasurer(angle=theta, radiation_pattern=pattern)
# Main lobe properties
main = lobes.main_lobe
print('Direction (deg):', np.degrees(main['direction']))
print('HPBW (deg):', np.degrees(main['beamwidth']))
# Side-lobe level (max side lobe / main lobe)
print('SLL:', lobes.sidelobe_level)
# Full table of all detected lobes
print(lobes.lobe_measures)

The antenna gain or directivity can be plotted in 3D using plotly for an interactive view. The pattern is mapped onto a unit sphere and colored by the gain in dB.

import plotly.graph_objects as go
# Select gain in dB at the target frequency
gain_dB = 10 * np.log10(np.abs(antenna_data.gain.sel(f=f0, method='nearest').squeeze()))
# Normalize the radius to the dB range of interest
dB_min, dB_max = -20, gain_dB.max().item()
r = np.clip((gain_dB.values - dB_min) / (dB_max - dB_min), 0, 1)
# Convert (theta, phi) grid to Cartesian for the 3D surface
theta, phi = gain_dB.theta.values, gain_dB.phi.values
phi_grid, theta_grid = np.meshgrid(phi, theta)
X = r * np.sin(theta_grid) * np.cos(phi_grid)
Y = r * np.sin(theta_grid) * np.sin(phi_grid)
Z = r * np.cos(theta_grid)
# Render
fig = go.Figure(data=go.Surface(x=X, y=Y, z=Z, surfacecolor=gain_dB.values,
colorscale='Jet', colorbar=dict(title='Gain (dB)')))
fig.update_layout(scene=dict(aspectmode='data'))
fig.show()

Submitting a ModeSolver returns a MicrowaveModeSolverData object containing the solved modes and the field profiles at the port plane. Both WavePort and TerminalWavePort can be converted to a mode solver via port.to_mode_solver() for previewing the port modes before the full 3D run.

# Build and run a mode solver for a wave port
mode_solver = my_wave_port.to_mode_solver(simulation = my_sim, freqs=np.linspace(1e9, 10e9, 11))
mode_data = web.run(mode_solver, task_name='my mode solver')

Key attributes of the result, indexed by (f, mode_index):

  • n_eff — real part of the complex effective index
  • k_eff — imaginary part of the complex effective index (loss)
  • n_complex — full complex effective index
  • gamma — complex propagation constant, with related alpha (attenuation) and beta (phase constant) properties

The transmission-line characteristic impedance Z0Z_0 is also reported when the mode solver is created by a WavePort or TerminalWavePort. It is stored on a separate sub-dataset:

  • transmission_line_data.Z0 — for WavePort, a 1D array indexed by (f, mode_index)
  • transmission_line_terminal_data.Z0 — for TerminalWavePort, a 2D matrix indexed by (f, terminal_label_out, terminal_label_in) to support coupled impedances between terminals
# Common attributes (work for both WavePort and TerminalWavePort mode data)
n_eff_data = mode_data.n_eff # (f, mode_index)
gamma_data = mode_data.gamma # (f, mode_index)
n_eff_at_mode0 = mode_data.n_eff.sel(mode_index=0)
# WavePort: characteristic impedance is a per-mode scalar
Z0_waveport = mode_data.transmission_line_data.Z0
Z0_mode0 = Z0_waveport.sel(mode_index=0)
# TerminalWavePort: characteristic impedance is a terminal-by-terminal matrix
Z0_terminal = mode_data.transmission_line_terminal_data.Z0
Z0_T0T0 = Z0_terminal.sel(terminal_label_out='T0', terminal_label_in='T0')

For a high-level summary of all solved modes (including wavelength, effective index, loss, mode area, TE fraction, and group index/dispersion), use the modes_info property.

# xarray.Dataset summary of all modes
print(mode_data.modes_info)
# Convert to a pandas DataFrame (table format)
df = mode_data.to_dataframe()

The mode profile can be visualized using ModeSolver.plot_field().

# Plot |Ex| of mode_index 0 at frequency f0
mode_solver.plot_field('Ex', val='abs', mode_index=0, f=f0, ax=ax)
# Plot the in-plane vector magnitude |E| of mode_index 1
mode_solver.plot_field('E', val='abs', mode_index=1, f=f0, ax=ax)

The ImpedanceCalculator computes the characteristic impedance of a transmission-line mode from user-defined voltage and current path integrals. This is useful for cross-checking the impedance reported by a TerminalWavePort, or for extracting impedance from a custom mode profile.

# Voltage integral: line integral of E from one conductor to the other
v_int = AxisAlignedVoltageIntegral(
center=(0, 0, 0),
size=(0, 0, 100), # 1D integration path along z
sign='+',
extrapolate_to_endpoints=True,
snap_path_to_grid=True,
)
# Current integral: closed contour of H around the signal conductor
i_int = AxisAlignedCurrentIntegral(
center=(0, 0, 50),
size=(200, 200, 0), # 2D contour in the xy-plane
sign='+',
extrapolate_to_endpoints=True,
snap_contour_to_grid=True,
)
# Assemble and evaluate based on mode solver data
calc = ImpedanceCalculator(voltage_integral=v_int, current_integral=i_int)
Z0 = calc.compute_impedance(mode_data)

Both voltage_integral and current_integral are optional, but at least one must be provided. When both are specified, Z0=V/IZ_0 = V / I. When only one is given, the other quantity is inferred from the modal power flow. For non-axis-aligned geometries, use the Custom2DVoltageIntegral and Custom2DCurrentIntegral variants.