Palace Field Visualization¶
Top-view and cross-section visualization of electromagnetic fields from a Palace driven simulation on a CPW (coplanar waveguide) structure at 50 GHz.
Requirements:
- IHP PDK:
uv pip install ihp-gdsfactory - GDSFactory+ account for cloud simulation
Note: This notebook now uses :func:gsim.palace.fields.plot_boundary_field
for NaN-free direct-mesh rendering (replaces the old probe-grid resampling
approach that produced NaN values and poor-quality plots).
Simulation setup¶
import gdsfactory as gf
from ihp import LAYER, PDK
from gsim.common.stack import get_stack
from gsim.palace import DrivenSim
PDK.activate()
@gf.cell
def gsg_electrode(
length=300, s_width=20, g_width=40, gap_width=15, layer=LAYER.TopMetal2drawing
):
c = gf.Component()
r1 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)
r1.move((0, (g_width + s_width) / 2 + gap_width))
_r2 = c << gf.c.rectangle((length, s_width), centered=True, layer=layer)
r3 = c << gf.c.rectangle((length, g_width), centered=True, layer=layer)
r3.move((0, -(g_width + s_width) / 2 - gap_width))
c.add_port(
name="o1",
center=(-length / 2, 0),
width=s_width,
orientation=180,
port_type="electrical",
layer=layer,
)
c.add_port(
name="o2",
center=(length / 2, 0),
width=s_width,
orientation=0,
port_type="electrical",
layer=layer,
)
return c
sim = DrivenSim()
sim.set_output_dir("./palace-sim-cpw-fields")
sim.set_geometry(gsg_electrode())
stack = get_stack(
include_substrate=True, substrate_thickness=2.0
) # auto-detects active PDK
sim.set_stack(stack)
sim.add_cpw_port("o1", layer="topmetal2", s_width=20, gap_width=15)
sim.add_cpw_port("o2", layer="topmetal2", s_width=20, gap_width=15)
# Single frequency point at 50 GHz, adaptive off so Palace does a full solve
sim.set_driven(
fmin=50e9,
fmax=50e9 + 1e6, # tiny range = effectively one point
num_points=1,
adaptive_tol=0,
save_step=1,
)
sim.set_airbox(margin_x=50, margin_y=0, z_above=100, z_below=100)
sim.mesh(
preset="default",
refined_mesh_size=2.0,
max_mesh_size=25.0,
)
Mesh Summary
========================================
Dimensions: 500.0 x 130.0 x 217.9 µm
Nodes: 16,745
Elements: 124,068
Tetrahedra: 90,126
Edge length: 1.12 - 51.35 µm
Quality: 0.645 (min: 0.012)
SICN: 0.692 (all valid)
----------------------------------------
Volumes (4):
- silicon [1]
- si [2]
- sin [3]
- air [4]
Surfaces (15):
- topmetal2_xy [5]
- topmetal2_z [6]
- P1_E0 [7]
- P1_E1 [8]
- P2_E0 [9]
- P2_E1 [10]
- air__silicon [11]
- silicon__None [12]
- si__silicon [13]
- air__si [14]
- si__None [15]
- si__sin [16]
- air__sin [17]
- sin__None [18]
- air__None [19]
----------------------------------------
Mesh: palace-sim-cpw-fields/palace.msh
Info : Reading 'palace-sim-cpw-fields/palace.msh'...
Info : 16745 nodes
Info : 120892 elements
Info : Done reading 'palace-sim-cpw-fields/palace.msh'
Info : Reading 'palace-sim-cpw-fields/palace.msh'...
Info : 16745 nodes
Info : 120892 elements
Info : Done reading 'palace-sim-cpw-fields/palace.msh'
Info : Reading 'palace-sim-cpw-fields/palace.msh'...
Info : 16745 nodes
Info : 120892 elements
Info : Done reading 'palace-sim-cpw-fields/palace.msh'
Running Palace simulation in palace-sim-cpw-fields via Apptainer
Command: apptainer run /opt/palace/Palace.sif -np 16 config.json
Processes: 16
Info : Reading 'palace-sim-cpw-fields/palace.msh'...
Info : 16745 nodes
Info : 120892 elements
Info : Done reading 'palace-sim-cpw-fields/palace.msh'
>> /usr/lib64/mpich/bin/mpirun -n 16 /opt/palace/bin/palace-x86_64.bin config.json
_____________ _______
_____ __ \____ __ /____ ____________
____ /_/ / __ ` / / __ ` / ___/ _ \
___ _____/ /_/ / / /_/ / /__/ ___/
/__/ \___,__/__/\___,__/\_____\_____/
[38;2;255;255;000m--> Warning![0m
Output folder is not empty; program will overwrite content! (output/palace)
Git changeset ID: v0.14.0-270-g9d6ea72f
Running with 16 MPI processes, 1 OpenMP thread
Device configuration: omp,cpu
Memory configuration: host-std
libCEED backend: /cpu/self/xsmm/blocked
Added 5864 duplicate vertices for interior boundaries in the mesh
Added 11144 duplicate boundary elements for interior boundaries in the mesh
Finished partitioning mesh into 16 subdomains
Characteristic length and time scales:
L₀ = 5.000e-04 m, t₀ = 1.668e-03 ns
Mesh curvature order: 1
Mesh bounding box:
(Xmin, Ymin, Zmin) = (-2.500e-04, -6.500e-05, -1.020e-04) m
(Xmax, Ymax, Zmax) = (+2.500e-04, +6.500e-05, +1.159e-04) m
Parallel Mesh Stats:
minimum average maximum total
vertices 1004 1413 2183 22609
edges 6602 7930 10096 126886
faces 11069 12150 13647 194408
elements 5471 5632 5802 90126
neighbors 0 5 10
minimum maximum
h 0.00225773 0.0821941
kappa 1.09182 218.824
Configuring Robin absorbing BC (order 2) at attributes:
12, 15, 18-19
Configuring Robin finite conductivity BC at attributes:
5: σ = 3.770e+07 S/m, h = 3.000e-06 m, n = (-0.0,+0.0,+1.0)
6: σ = 3.770e+07 S/m, h = 3.000e-06 m, n = (-0.0,+1.0,+0.0)
Configuring Robin impedance BC for lumped ports at attributes:
7: Rs = 1.333e+01 Ω/sq, n = (+0.0,-0.0,+1.0)
8: Rs = 1.333e+01 Ω/sq, n = (+0.0,+0.0,+1.0)
9: Rs = 1.333e+01 Ω/sq, n = (-0.0,+0.0,+1.0)
10: Rs = 1.333e+01 Ω/sq, n = (-0.0,-0.0,+1.0)
Configuring lumped port circuit properties:
Index = 1: R = 5.000e+01 Ω
Index = 2: R = 5.000e+01 Ω
Configuring lumped port excitation source term at attributes:
7: Index = 1
8: Index = 1
9: Index = 2
10: Index = 2
Computing frequency response for:
Excitation 1/2 with index 1 has contributions from:
Lumped port 1
Excitation 2/2 with index 2 has contributions from:
Lumped port 2
Assembling system matrices, number of global unknowns:
H1 (p = 2): 149495, ND (p = 2): 642588, RT (p = 2): 853602
Operator assembly level: Partial
Mesh geometries:
Tetrahedron: P = 20, Q = 11 (quadrature order = 4)
SuperLUSolver: Using 2D processor grid 4 x 4
Sweeping excitation index 1 (1/2):
Assembling multigrid hierarchy:
Level 0 (p = 1): 126886 unknowns
Level 1 (p = 2): 642588 unknowns
Level 0 (auxiliary) (p = 1): 22609 unknowns
Level 1 (auxiliary) (p = 2): 149495 unknowns
It 1/2: ω/2π = 5.000e+01 GHz (total elapsed time = 6.28e+00 s, solve 1/4)
Residual norms for GMRES solve
matrix dimension 126886
nonzeros in A 1834090
nonzeros in L 17281828
nonzeros in U 17281828
nonzeros in L+U 34436770
fill ratio 18.8
nonzeros in LSUB 5718579
** Memory Usage **********************************
** Total highmark (MB):
Sum-of-all : 2034.29 | Avg : 127.14 | Max : 136.67
Max at rank 5, different stages (MB):
. symbfact 55.13
. distribution 125.16
. numfact 58.26
** NUMfact space (MB): (sum-of-all-processes)
L\U : 295.81 | Total : 941.94
. max at rank 5, max L+U memory (MB): 25.08
. max at rank 15, peak buffer (MB): 41.27
**************************************************
** number of Tiny Pivots: 0
0 (restart 0) KSP residual norm 6.076592e+01
1 (restart 0) KSP residual norm 5.109583e+00
2 (restart 0) KSP residual norm 1.359014e+00
3 (restart 0) KSP residual norm 9.562134e-01
4 (restart 0) KSP residual norm 1.455992e-01
5 (restart 0) KSP residual norm 7.300275e-02
6 (restart 0) KSP residual norm 2.922808e-02
7 (restart 0) KSP residual norm 1.859018e-02
8 (restart 0) KSP residual norm 5.416040e-03
9 (restart 0) KSP residual norm 3.159745e-03
10 (restart 0) KSP residual norm 1.124984e-03
11 (restart 0) KSP residual norm 6.398113e-04
12 (restart 0) KSP residual norm 2.986644e-04
13 (restart 0) KSP residual norm 1.773515e-04
14 (restart 0) KSP residual norm 8.209257e-05
15 (restart 0) KSP residual norm 5.345515e-05
GMRES solver converged in 15 iterations (avg. reduction factor: 3.947e-01)
Sol. ||E|| = 2.682384e+01 (||RHS|| = 2.609552e+00)
Field energy E (1.365e-03 J) + H (1.232e-03 J) = 2.597e-03 J
S[1][1] = -4.823e-02-3.059e-02i, |S[1][1]| = -2.487e+01, arg(S[1][1]) = -1.476e+02
S[2][1] = +6.290e-01-7.288e-01i, |S[2][1]| = -3.297e-01, arg(S[2][1]) = -4.920e+01
Wrote fields to disk (Paraview) at step 1
Updating solution error estimates
It 2/2: ω/2π = 5.000e+01 GHz (total elapsed time = 5.54e+01 s, solve 2/4)
Residual norms for GMRES solve
** Memory Usage **********************************
** Total highmark (MB):
Sum-of-all : 1335.57 | Avg : 83.47 | Max : 90.65
Max at rank 5, different stages (MB):
. symbfact 0.00
. distribution 81.89
. numfact 58.26
** NUMfact space (MB): (sum-of-all-processes)
L\U : 295.81 | Total : 941.94
. max at rank 5, max L+U memory (MB): 25.08
. max at rank 15, peak buffer (MB): 41.27
**************************************************
** number of Tiny Pivots: 0
0 (restart 0) KSP residual norm 6.673979e-04
1 (restart 0) KSP residual norm 1.239932e-04
2 (restart 0) KSP residual norm 3.474756e-05
GMRES solver converged in 2 iterations (avg. reduction factor: 7.562e-04)
Sol. ||E|| = 2.682381e+01 (||RHS|| = 2.609605e+00)
Field energy E (1.365e-03 J) + H (1.232e-03 J) = 2.597e-03 J
S[1][1] = -4.823e-02-3.059e-02i, |S[1][1]| = -2.487e+01, arg(S[1][1]) = -1.476e+02
S[2][1] = +6.290e-01-7.288e-01i, |S[2][1]| = -3.297e-01, arg(S[2][1]) = -4.920e+01
Wrote fields to disk (Paraview) at step 2
Updating solution error estimates
Sweeping excitation index 2 (2/2):
It 1/2: ω/2π = 5.000e+01 GHz (total elapsed time = 8.45e+01 s, solve 3/4)
Residual norms for GMRES solve
** Memory Usage **********************************
** Total highmark (MB):
Sum-of-all : 1335.57 | Avg : 83.47 | Max : 90.65
Max at rank 5, different stages (MB):
. symbfact 0.00
. distribution 81.89
. numfact 58.26
** NUMfact space (MB): (sum-of-all-processes)
L\U : 295.81 | Total : 941.94
. max at rank 5, max L+U memory (MB): 25.08
. max at rank 15, peak buffer (MB): 41.27
**************************************************
** number of Tiny Pivots: 0
0 (restart 0) KSP residual norm 9.044130e+00
1 (restart 0) KSP residual norm 9.015920e-01
2 (restart 0) KSP residual norm 3.971593e-01
3 (restart 0) KSP residual norm 1.941001e-01
4 (restart 0) KSP residual norm 1.341383e-01
5 (restart 0) KSP residual norm 3.645206e-02
6 (restart 0) KSP residual norm 1.713165e-02
7 (restart 0) KSP residual norm 1.207056e-02
8 (restart 0) KSP residual norm 3.717866e-03
9 (restart 0) KSP residual norm 1.928589e-03
10 (restart 0) KSP residual norm 9.327876e-04
11 (restart 0) KSP residual norm 4.515979e-04
12 (restart 0) KSP residual norm 2.215293e-04
13 (restart 0) KSP residual norm 1.249671e-04
14 (restart 0) KSP residual norm 6.457379e-05
15 (restart 0) KSP residual norm 3.772007e-05
GMRES solver converged in 15 iterations (avg. reduction factor: 3.857e-01)
Sol. ||E|| = 2.682447e+01 (||RHS|| = 2.623449e+00)
Field energy E (1.365e-03 J) + H (1.232e-03 J) = 2.597e-03 J
S[1][2] = +6.290e-01-7.288e-01i, |S[1][2]| = -3.297e-01, arg(S[1][2]) = -4.920e+01
S[2][2] = -4.824e-02-3.057e-02i, |S[2][2]| = -2.487e+01, arg(S[2][2]) = -1.476e+02
Wrote fields to disk (Paraview) at step 1
Updating solution error estimates
It 2/2: ω/2π = 5.000e+01 GHz (total elapsed time = 1.32e+02 s, solve 4/4)
Residual norms for GMRES solve
** Memory Usage **********************************
** Total highmark (MB):
Sum-of-all : 1335.57 | Avg : 83.47 | Max : 90.65
Max at rank 5, different stages (MB):
. symbfact 0.00
. distribution 81.89
. numfact 58.26
** NUMfact space (MB): (sum-of-all-processes)
L\U : 295.81 | Total : 941.94
. max at rank 5, max L+U memory (MB): 25.08
. max at rank 15, peak buffer (MB): 41.27
**************************************************
** number of Tiny Pivots: 0
0 (restart 0) KSP residual norm 6.668819e-04
1 (restart 0) KSP residual norm 1.258537e-04
2 (restart 0) KSP residual norm 2.884878e-05
GMRES solver converged in 2 iterations (avg. reduction factor: 6.893e-04)
Sol. ||E|| = 2.682445e+01 (||RHS|| = 2.623501e+00)
Field energy E (1.365e-03 J) + H (1.232e-03 J) = 2.597e-03 J
S[1][2] = +6.290e-01-7.288e-01i, |S[1][2]| = -3.297e-01, arg(S[1][2]) = -4.920e+01
S[2][2] = -4.825e-02-3.057e-02i, |S[2][2]| = -2.487e+01, arg(S[2][2]) = -1.476e+02
Wrote fields to disk (Paraview) at step 2
Updating solution error estimates
Completed 0 iterations of adaptive mesh refinement (AMR):
Indicator norm = 1.118e-01, global unknowns = 642588
Max. iterations = 0, tol. = 1.000e-02
Elapsed Time Report (s) Min. Max. Avg.
==============================================================
Initialization 0.116 0.125 0.123
Mesh Preprocessing 1.160 1.161 1.161
Operator Construction 0.476 0.512 0.497
Linear Solve 5.973 9.132 6.580
Setup 24.713 24.727 24.721
Preconditioner 39.280 45.907 44.924
Coarse Solve 12.300 15.969 12.873
Estimation 0.368 0.395 0.381
Construction 8.610 8.643 8.630
Solve 50.622 50.657 50.637
Postprocessing 0.604 0.642 0.613
Paraview 12.883 12.915 12.906
Disk IO 0.604 0.607 0.606
--------------------------------------------------------------
Total 164.704 164.706 164.704
Simulation completed successfully
Results saved to palace-sim-cpw-fields/output/palace
Load results and setup¶
from pathlib import Path
import numpy as np
import pyvista as pv
# New NaN-free field-visualization module (replaces old gsim.viz.plot_topview/plot_cross_section)
from gsim.palace.fields import (
load_boundary_field_data,
load_field_context,
load_volume_field_data,
plot_boundary_field,
plot_volume_slice,
)
pv.OFF_SCREEN = True
# Get results dir from sim output (or hardcode for re-runs)
results_dir = Path(results.files["port-S.csv"]).parent
print(f"Results dir: {results_dir}")
# Read frequency from S-parameter CSV
s_csv = np.loadtxt(results_dir / "port-S.csv", delimiter=",", skiprows=1)
freq_ghz = s_csv[0, 0]
# Load volume + boundary meshes and build the SelectorContext in one call
vol, bnd, ctx, pg_map = load_field_context(results_dir, excitation=1)
print(f"Frequency: {freq_ghz:.1f} GHz")
print(f"Volume: {vol.n_points:,} points, {vol.n_cells:,} cells")
print(f"Boundary: {bnd.n_points:,} points, {bnd.n_cells:,} cells")
# Resolve topmetal2_xy attributes for surface-current plot
topmetal2_attrs = [
int(tag) for name, tag in pg_map.items() if "topmetal2_xy" in name.lower()
]
print(f"topmetal2_xy attributes: {topmetal2_attrs}")
print(f"Known entity names: {sorted(pg_map.keys())}")
# Top-view conductor z-plane
z_conductor = 16.0
Results dir: palace-sim-cpw-fields/output/palace
Frequency: 50.0 GHz
Volume: 901,260 points, 90,126 cells
Boundary: 251,460 points, 41,910 cells
topmetal2_xy attributes: [5]
Known entity names: ['P1_E0', 'P1_E1', 'P2_E0', 'P2_E1', 'air', 'air__None', 'air__si', 'air__silicon', 'air__sin', 'si', 'si__None', 'si__silicon', 'si__sin', 'silicon', 'silicon__None', 'sin', 'sin__None', 'topmetal2_xy', 'topmetal2_z']
Top-view volume slices at conductor layer (NaN-free direct mesh rendering)¶
# |E| volume slice at z_conductor — uses extract_axis_slice + direct mesh rendering (no NaN)
vol_data = load_volume_field_data(results_dir, excitation=1)
pl_e = plot_volume_slice(
vol_data,
vector_field="E_real",
component="mag",
axis="z",
value=z_conductor,
cmap="turbo",
scalar_bar_title=f"|E| @ {freq_ghz:.1f} GHz — volume slice (V/m)",
off_screen=True,
)
pl_e.show(jupyter_backend="static")

# |S| Poynting vector — power flow along the waveguide
pl_s_vol = plot_volume_slice(
vol_data,
vector_field="S",
component="mag",
axis="z",
value=z_conductor,
cmap="turbo",
scalar_bar_title=f"|S| @ {freq_ghz:.1f} GHz — power flow (W/m²)",
off_screen=True,
)
pl_s_vol.show(jupyter_backend="static")

Surface-current boundary plot (NaN-free direct mesh rendering)¶
This uses :func:gsim.palace.fields.plot_boundary_field which renders the
actual boundary mesh cells directly — no resampling to a regular grid —
so there are zero NaN values.
# Load boundary field data — only topmetal2_xy conductor faces
bnd_data = load_boundary_field_data(
results_dir,
ctx,
attributes=topmetal2_attrs,
excitation=1,
)
print(
f"Boundary mesh: {bnd_data.mesh.n_points:,} points, {bnd_data.mesh.n_cells:,} cells"
)
print(f"Available fields: {sorted(bnd_data.point_arrays)}")
# Explicit NaN check — the whole point
if "J_s_real" in bnd_data.point_arrays:
from gsim.palace.fields import activate_vector_component
scalar_name = activate_vector_component(bnd_data.mesh, "J_s_real", component="mag")
n_nan = int(np.isnan(bnd_data.mesh.point_data[scalar_name]).sum())
print(f" NaN count in |J_s_real|: {n_nan} / {bnd_data.mesh.n_points:,} points")
assert n_nan == 0, f"Expected 0 NaN values, got {n_nan}"
Boundary mesh: 100,944 points, 16,824 cells
Available fields: ['B_imag', 'B_real', 'E_imag', 'E_real', 'J_s_imag', 'J_s_real', 'Q_s_imag', 'Q_s_real', 'S', 'U_e', 'U_m', 'vtkOriginalPointIds']
NaN count in |J_s_real|: 0 / 100,944 points
# Plot surface current — NO NaN, direct mesh rendering
pl = plot_boundary_field(
bnd_data,
vector_field="J_s_real",
component="mag",
cmap="rainbow",
log_scale=False,
opacity=0.95,
scalar_bar_title=f"|J_s| @ {freq_ghz:.1f} GHz — NaN-free direct mesh (A/m)",
show_edges=False,
off_screen=True,
)
# Orthographic top view
bounds = bnd_data.mesh.bounds
cx = 0.5 * (bounds[0] + bounds[1])
cy = 0.5 * (bounds[2] + bounds[3])
cz = 0.5 * (bounds[4] + bounds[5])
span_xy = max(bounds[1] - bounds[0], bounds[3] - bounds[2])
pl.camera_position = (cx, cy, cz + span_xy * 3.0)
pl.camera.focal_point = (cx, cy, cz)
pl.camera.up = (0.0, 1.0, 0.0)
pl.enable_parallel_projection()
pl.show(jupyter_backend="static")
[0m[33m2026-07-08 17:05:54.258 ( 176.538s) [ 760869D87080] vtkRenderer.cxx:1144 WARN| vtkOpenGLRenderer (0x439bc070): Resetting view-up since view plane normal is parallel[0m

Cross-sections — YZ plane at x=0¶
# |E| cross-section — uses 3D volume slice + direct mesh rendering
pl_cs_e = plot_volume_slice(
vol_data,
vector_field="E_real",
component="mag",
axis="x",
value=0.0,
cmap="turbo",
scalar_bar_title=f"|E| cross-section @ {freq_ghz:.1f} GHz (V/m)",
off_screen=True,
)
# Camera looks straight into the YZ plane (down the +X axis toward x=0)
_b = pl_cs_e.mesh.bounds
_cx, _cy, _cz = 0.0, 0.5 * (_b[2] + _b[3]), 0.5 * (_b[4] + _b[5])
_span_yz = max(_b[3] - _b[2], _b[5] - _b[4])
pl_cs_e.camera.position = (_cx + _span_yz * 3.0, _cy, _cz)
pl_cs_e.camera.focal_point = (_cx, _cy, _cz)
pl_cs_e.camera.up = (0.0, 0.0, 1.0)
pl_cs_e.enable_parallel_projection()
pl_cs_e.show(jupyter_backend="static")

# |B| cross-section — magnetic field circulating around conductor
pl_cs_b = plot_volume_slice(
vol_data,
vector_field="B_real",
component="mag",
axis="x",
value=0.0,
cmap="turbo",
scalar_bar_title=f"|B| cross-section @ {freq_ghz:.1f} GHz (T)",
off_screen=True,
)
# Camera looks straight into the YZ plane (down the +X axis toward x=0)
_b = pl_cs_b.mesh.bounds
_cx, _cy, _cz = 0.0, 0.5 * (_b[2] + _b[3]), 0.5 * (_b[4] + _b[5])
_span_yz = max(_b[3] - _b[2], _b[5] - _b[4])
pl_cs_b.camera.position = (_cx + _span_yz * 3.0, _cy, _cz)
pl_cs_b.camera.focal_point = (_cx, _cy, _cz)
pl_cs_b.camera.up = (0.0, 0.0, 1.0)
pl_cs_b.enable_parallel_projection()
pl_cs_b.show(jupyter_backend="static")

Field components — E_y at conductor layer¶
E_y is the dominant E-field component in a CPW — the transverse field across
the gaps between signal and ground. A diverging colormap shows the polarity
flipping between the two gaps.
# E_y component via plot_volume_slice with vector component 'y'
pl_ey = plot_volume_slice(
vol_data,
vector_field="E_real",
component="y",
axis="z",
value=z_conductor,
cmap="RdBu_r",
scalar_bar_title=f"E_y @ {freq_ghz:.1f} GHz — transverse field (V/m)",
off_screen=True,
)
pl_ey.show(jupyter_backend="static")
