Working with Results
This page describes how to work with simulation results in Flexcompute RF (also Flex RF).
Basic Concepts
Section titled “Basic Concepts”A successful simulation run returns a result data object whose type depends on the submitted simulation type.
| Submitted simulation | Result data object |
|---|---|
TerminalComponentModeler | TerminalComponentModelerData (TCMData) |
ModeSolver | MicrowaveModeSolverData |
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 resultstcm_data = web.run(my_tcm, task_name='my tcm job')
# Top-level methods on TCMDatas_matrix = tcm_data.smatrix() # S-parametersantenna_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 SimulationDatafield_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 batchbatch_results = web.run({'config_A': tcm_A, 'config_B': tcm_B}, task_name='my sweep')tcm_data_A = batch_results['config_A']Xarray
Section titled “Xarray”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 frequenciess11 = 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) directiong_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 NumPys11_array = s11.values
# Export to files_matrix.data.to_netcdf('s_matrix.nc')Port indexing
Section titled “Port indexing”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 LP1sim_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 modesmy_wave_port = WavePort( name='WP1', mode_spec=MicrowaveModeSpec(num_modes=2, ...), ...)
# Access data associated with WP1 and mode 0sim_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 pairmy_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 TWP1sim_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.
Port Data (S-parameters)
Section titled “Port Data (S-parameters)”The smatrix() method on TCMData returns a MicrowaveSMatrixData object containing the computed S-parameters.
# Compute the S-matrixs_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. S21s21 = 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-matrixz_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 matricesv_matrix, i_matrix = tcm_data.port_voltage_current_matricesThe 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 matrixz_ref = tcm_data.port_reference_impedancesRenormalization
Section titled “Renormalization”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 referencetcm_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.
De-embedding
Section titled “De-embedding”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 umport_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.
Monitor Data
Section titled “Monitor Data”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 namefield_data = sim_data['my field monitor']
# Access the underlying field components, indexed by (x, y, z, f)Ex = field_data.ExEz_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.
Surface Field/Current Data
Section titled “Surface Field/Current Data”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 datasurf_data = sim_data['my surface monitor']
# Underlying field datasetsE_surface = surf_data.EH_surface = surf_data.H
# Surface current densityJ_surface = surf_data.current_density
# Time-averaged Poynting vector on the surfacepoynting = surf_data.poyntingThe 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 f0J_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 f0J_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 f0H_at_f0 = surf_data.H.sel(f=f0, method='nearest', side='outside')H_at_f0.norm(dim='axis').plot()Antenna Data
Section titled “Antenna Data”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 portantenna_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 directivityradiation_intensity— radiation intensity in W/sraxial_ratio— polarization axial ratiopower_incident,power_reflected,supplied_power— power balanceradiation_efficiency,reflection_efficiency— antenna efficienciesEtheta,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 - and -polarized components) or pol_basis='circular' (returns RHCP and LHCP components).
# Linear polarization decompositionpg_linear = antenna_data.partial_gain(pol_basis='linear')g_theta = pg_linear.Gthetag_phi = pg_linear.Gphi
# Circular polarization decompositionpg_circ = antenna_data.partial_gain(pol_basis='circular')g_RHCP = pg_circ.Grightg_LHCP = pg_circ.GleftPort Feed Patterns
Section titled “Port Feed Patterns”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 is the incident power into the port.
# Differential excitation: port_1 driven with amplitude 1, port_2 with -1port_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 directionN = 8beam_phase = np.pi / 4 # progressive phase shift between elementsport_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.
Lobe Measurer
Section titled “Lobe Measurer”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 cuttheta = 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 propertiesmain = lobes.main_lobeprint('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 lobesprint(lobes.lobe_measures)3D Gain/Directivity Plot
Section titled “3D Gain/Directivity Plot”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 frequencygain_dB = 10 * np.log10(np.abs(antenna_data.gain.sel(f=f0, method='nearest').squeeze()))
# Normalize the radius to the dB range of interestdB_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 surfacetheta, phi = gain_dB.theta.values, gain_dB.phi.valuesphi_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)
# Renderfig = 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()2D Mode Data
Section titled “2D Mode Data”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 portmode_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 indexk_eff— imaginary part of the complex effective index (loss)n_complex— full complex effective indexgamma— complex propagation constant, with relatedalpha(attenuation) andbeta(phase constant) properties
The transmission-line characteristic impedance 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— forWavePort, a 1D array indexed by(f, mode_index)transmission_line_terminal_data.Z0— forTerminalWavePort, 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 scalarZ0_waveport = mode_data.transmission_line_data.Z0Z0_mode0 = Z0_waveport.sel(mode_index=0)
# TerminalWavePort: characteristic impedance is a terminal-by-terminal matrixZ0_terminal = mode_data.transmission_line_terminal_data.Z0Z0_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 modesprint(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 f0mode_solver.plot_field('Ex', val='abs', mode_index=0, f=f0, ax=ax)
# Plot the in-plane vector magnitude |E| of mode_index 1mode_solver.plot_field('E', val='abs', mode_index=1, f=f0, ax=ax)Impedance Calculator
Section titled “Impedance Calculator”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 otherv_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 conductori_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 datacalc = 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, . 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.