---
title: 'Co-planar Waveguide in RF Photonics 1: Transmission Line Basics'
jupyter: python3
---

<center><img src="./img/cpw_rf_photonics_1_render.png" width=480 /></center>

The co-planar waveguide (CPW) is a transmission line commonly used in RF photonics due to its compact planar nature, ease of fabrication, and resistance to external EM interference. In this notebook series, we will examine the CPW in the context of a Mach-Zehnder modulator (MZM).

In part one (this notebook), we will start with a simple CPW on a dielectric substrate. The goal is to demonstrate the typical 2D and 3D workflow in Flex RF.

In [part two](../cpw-rf-photonics-2/), we will simulate the CPW with the MZM optical waveguide. A 2D mode analysis will first be performed on the conventional layout, followed by a 3D simulation of a segmented electrode design.

```{python}
import matplotlib.pyplot as plt
import numpy as np
import flex_rf.tidy3d as rf
import flex_rf.web as web
rf.config.logging.level = 'ERROR'
```

## Building the simulation

### Key parameters

We start by defining some key parameters.

```{python}
# Frequency range
f_min, f_max = (1e9, 65e9)
f0 = (f_max + f_min) / 2
freqs = np.linspace(f_min, f_max, 201)

# Key dimensions (default units: um)
# -- CPW --
G = 5  # CPW gap
WS = 30  # Signal trace width
WG = 350  # Ground trace width
W_cpw = WS + 2 * (G + WG)  # Total CPW width
T = 1  # Conductor thickness
L = 2000  # Line length (distance between wave ports)
# -- Substrate --
L_sub, W_sub, H_sub = (L + 200, 2400, 1000)  # Substrate length, width, thickness
```

### Material and Geometry

The substrate is assumed to be lossless. The metal is assumed to have constant conductivity over the frequency range.

```{python}
# Material properties
cond = 41  # Conductivity in S/um
eps = 4.5  # Relative permittivity, substrate

# Define EM mediums
med_sub = rf.Medium(permittivity=eps)
med_metal = rf.LossyMetalMedium(conductivity=cond, frequency_range=(f_min, f_max))
```

The CPW geometry is created below.

```{python}
# Create substrate
str_sub_layer = rf.Structure(
    geometry=rf.Box(center=(0, -H_sub / 2, 0), size=(W_sub, H_sub, L_sub)),
    medium=med_sub,
)

# Create CPW
geom_sig = rf.Box(center=(0, T / 2, 0), size=(WS, T, L))
geom_gnd1 = rf.Box.from_bounds(rmin=(-WS / 2 - G - WG, 0, -L / 2), rmax=(-WS / 2 - G, T, L / 2))
geom_gnd2 = geom_gnd1.reflected((1, 0, 0))
geom_cpw = rf.GeometryGroup(geometries=[geom_sig, geom_gnd1, geom_gnd2])
str_cpw = rf.Structure(geometry=geom_cpw, medium=med_metal)

# Full structure list
structure_list = [str_sub_layer, str_cpw]
```

### Grid and Boundary

The simulation domain is surrounded by Perfectly Matched Layers (PMLs) by default. The simulation domain size is defined below.

```{python}
# Define simulation size
sim_LX = W_sub
sim_LY = 2 * H_sub
sim_LZ = L_sub
```

The simulation grid is automatically generated based on the minimum wavelength in the respective dielectric medium. In RF problems, the field is especially enhanced near metallic structures. For more accurate results, we use the `LayerRefinementSpec` feature to automatically refine the grid near metallic edges and corners.

```{python}
# Define Layer refinement spec
lr_spec = rf.LayerRefinementSpec.from_structures(
    structures=[str_cpw],
    min_steps_along_axis=5,
    corner_refinement=rf.GridRefinement(dl=T / 5, num_cells=2),
)

# Overall grid specification
grid_spec = rf.GridSpec.auto(
    min_steps_per_wvl=15,
    wavelength=rf.C_0 / f_max,
    layer_refinement_specs=[lr_spec],
)
```

### Monitors

We define a field monitor in the CPW plane for visualization purposes below.

```{python}
mon_1 = rf.FieldMonitor(
    center=(0, 0, 0), size=(rf.inf, 0, rf.inf), freqs=[f0], name="field cpw plane"
)

monitor_list = [mon_1]
```

### Wave Ports

Wave ports are used to excite the CPW. It is important to ensure that the width of the ports are slightly smaller than the overall width spanned by the CPW side ground traces. In the image below, the recommended wave port positioning is demarcated by dashed lines, and the CPW metal structures are in copper. 

<center><img src="./img/wave_port_cpw.png" width=360 /></center>

```{python}
# Define wave ports
WP1 = rf.WavePort(
    center=(0, 0, -L / 2),
    size=(W_cpw - 50, W_cpw - 50, 0),
    direction="+",
    name="WP1",
    extrude_structures=True,
)
WP2 = WP1.updated_copy(
    center=(0, 0, L / 2),
    direction="-",
    name="WP2",
)

# List of all ports
port_list = [WP1, WP2]
```

### Define Simulation and `TerminalComponentModeler`

The base `Simulation` object contains information about the overall simulation environment. The `TerminalComponentModeler` object is used in 3D simulations to conduct a port and frequency sweep in order to obtain the full S-parameter matrix.

```{python}
# Define base simulation
sim = rf.Simulation(
    size=(sim_LX, sim_LY, sim_LZ),
    grid_spec=grid_spec,
    structures=structure_list,
    monitors=monitor_list,
    run_time=0.5e-9,
)

# Define TerminalComponentModeler
tcm = rf.TerminalComponentModeler(
    simulation=sim,
    ports=port_list,
    freqs=freqs,
    run_only=["WP1@0"],
)
```

The `run_only` parameter allows the user to sweep only a subset of the available port excitations. In this case, we only want to run wave port `WP1` and mode index `0` (the only mode). This is because the structure is symmetric, so there is no need to solve for port `WP2`.

### Visualization

We can inspect the grid to ensure a suitable level of refinement. The plots below show the full CPW (left) and the CPW gap (right). The green region is the wave port and the grid lines are in gray. The purple lines indicate regions of grid refinement.

```{python}
# Inspect transverse grid
fig, ax = plt.subplots(1, 2, figsize=(10, 6))
tcm.plot_sim(z=-L / 2, ax=ax[0], monitor_alpha=0)
sim.plot_grid(z=-L / 2, ax=ax[0], hlim=(-500, 500), vlim=(-500, 500))
sim.plot(z=-L / 2, ax=ax[1], monitor_alpha=0)
sim.plot_grid(z=-L / 2, ax=ax[1], hlim=(10, 30), vlim=(-10, 10))
plt.show()
```

```{python}
# Plot top view and wave ports (green/blue)
fig, ax = plt.subplots(figsize=(12, 5))
sim.plot_grid(y=0, ax=ax)
tcm.plot_sim(
    y=0.0, ax=ax, monitor_alpha=0, vlim=(-sim_LZ / 2, sim_LZ / 2), hlim=(-W_cpw * 0.6, 0.6 * W_cpw)
)
ax.set_aspect(0.2)
plt.show()
```

## 2D Analysis

In this section, we will perform a 2D mode analysis on the CPW and extract key transmission line parameters.

### Run Mode Solver

The 2D mode analysis is performed with a `ModeSolver` object. We can easily create one from the wave port using the `to_mode_solver()` method.

```{python}
# Obtain mode solver from wave port
mode_solver = WP1.to_mode_solver(simulation=sim, freqs=np.linspace(f_min, f_max, 51))
```

We run the mode solver below.

```{python}
# Run mode solver
mode_data = web.run(mode_solver, task_name="CPW mode solver", path="data/WP1_mode_data.hdf5", verbose=False)
```

### Mode Profile

First, let us examine the mode profile. The left plot depicts $\text{Re}(E_x)$ while the right plot shows the field magnitude in dB. 

```{python}
# Preview mode field
fig, ax = plt.subplots(1, 2, figsize=(10, 4), tight_layout=True)
mode_solver.plot_field(field_name="Ex", val="real", f=f_max, mode_index=0, ax=ax[0])
mode_solver.plot_field(
    field_name="E", val="abs^2", scale="dB", f=f_max, mode_index=0, ax=ax[1], vmin=-40, vmax=0
)
for axis in ax:
    axis.set_xlim(-200, 200)
    axis.set_ylim(-200, 200)
plt.show()
```

### Effective Index, Loss, and Line Impedance

The key properties that determine a guided mode are effective index $n_\text{eff}$, attenuation $\alpha$, and characteristic impedance $Z_0$. We demonstrate how to obtain those values below. We also demonstrate how to extract the complex propagation constant $\gamma$. Note the use of the `-np.conjugate()` operation to convert from physics phase convention (used in Flex RF) to engineering convention. 

```{python}
# Gather alpha, neff from mode solver results
neff_mode = mode_data.modes_info["n eff"].squeeze()
alphadB_mode = mode_data.modes_info["loss (dB/cm)"].squeeze()
# Get gamma in 1/mm
gamma_mode = -np.conjugate(mode_data.gamma.isel(mode_index=0)) * 1e-3
```

The characteristic line impedance $Z_0$ is retrieved from the mode solver data as well.

```{python}
# Retrieve the characteristic impedance from the mode impedance.
Z0_mode = np.conjugate(mode_data.transmission_line_data.Z0.isel(mode_index=0)).squeeze()
```

We plot the key transmission line metrics below. 

```{python}
# Comparison plots
fig, ax = plt.subplots(3, 1, figsize=(8, 8), tight_layout=True)
ax[0].set_title("Effective index")
ax[0].plot(neff_mode.f/1e9, neff_mode)
ax[1].set_title("Attenuation (dB/cm)")
ax[1].plot(alphadB_mode.f/1e9, alphadB_mode)
ax[2].set_title("Real impedance (Ohm)")
ax[2].plot(Z0_mode.f/1e9, np.real(Z0_mode))
for axis in ax:
    axis.grid()
    axis.set_xlabel("f (GHz)")
plt.show()
```

### Distributed RLCG transmission line parameters

Using the complex propagation constant $\gamma$ and the line impedance $Z_0$, we can calculate the RLCG parameters using the following relationships:
$$
  R = \text{Re}(\gamma Z_0), \quad L = \text{Im}(\gamma Z_0)/\omega, \quad G = \text{Re}(\gamma/Z_0), \quad C = \text{Im}(\gamma/Z_0)/\omega
$$
where $\omega = 2\pi f$. Since the dielectric is lossless, we can omit $G$. 

```{python}
R_mode = np.real(gamma_mode * Z0_mode)# Ohm/mm
L_mode = np.imag(gamma_mode * Z0_mode) / (2 * np.pi * gamma_mode.f) # H/mm
C_mode = np.imag(gamma_mode / Z0_mode) / (2 * np.pi * gamma_mode.f) # F/mm
```

We plot $R, L, C$ below. 

```{python}
# Plot RLC
fig, ax = plt.subplots(1,3 , figsize=(10, 3), tight_layout=False)
ax[0].plot(R_mode.f / 1e9, R_mode)
ax[1].plot(L_mode.f / 1e9, L_mode * 1e9)
ax[2].plot(C_mode.f / 1e9, C_mode * 1e12)
titles = ["R (Ohm/mm)", "L (nH/mm)", "C (pF/mm)"]
for ii, title in enumerate(titles):
    ax[ii].set_title(title)
    ax[ii].set_xlabel("f (GHz)")
    ax[ii].grid()
plt.show()
```

## 3D Analysis

### Run Simulation

Let us simulate a short section of the CPW to demonstrate the 3D workflow. First, the grid resolution can be reduced from the 2D case. This is because S-parameters generally converge much more quickly than modal parameters. Below, we redefine the grid specification for the 3D case.

```{python}
# Layer refinement spec
lr_spec_3d = rf.LayerRefinementSpec.from_structures(
    structures=[str_cpw],
    min_steps_along_axis=2,
    corner_refinement=rf.GridRefinement(dl=G / 3, num_cells=2),
)

# Overall grid specification
grid_spec_3d = rf.GridSpec.auto(
    min_steps_per_wvl=20,
    wavelength=rf.C_0 / f_max,
    layer_refinement_specs=[lr_spec_3d],
)
```

We make an updated copy of the base `Simulation` and `TerminalComponentModeler` using the reduced grid specification.

```{python}
sim_3d = sim.updated_copy(grid_spec=grid_spec_3d)
tcm_3d = tcm.updated_copy(simulation=sim_3d)
```

The simulation is executed below.

```{python}
tcm_data = web.run(tcm_3d, task_name="CPW 3D", path="data/tcm_data_cpw.hdf5", verbose=False)
```

### S-parameters

The S-matrix can be calculated using the `smatrix()` method from the `TerminalComponentModelerData` instance `tcm_data` returned by `web.run()`.

```{python}
smat = tcm_data.smatrix()
```

Below, we define some convenience functions to extract the individual $S_{ij}$ entries. The `port_in` and `port_out` coordinates are used to specify the port name/index. Note the use of `np.conjugate()` to convert the S-parameter from the physics phase convention to the standard electrical engineering convention.

```{python}
def sparam(i, j):
    return np.conjugate(smat.data.isel(port_in=j - 1, port_out=i - 1))

def sparam_dB(i, j):
    return 20 * np.log10(np.abs(sparam(i, j)))
```

The insertion and return losses are shown below. 

```{python}
fig, ax=plt.subplots(2,1,figsize=(8, 6), tight_layout=True)
ax[0].plot(freqs/1e9, sparam_dB(2,1), label='S21')
ax[0].set_title('Insertion loss')
ax[1].plot(freqs/1e9, sparam_dB(1,1), label='S11')
ax[1].set_title('Return loss')
for axis in ax:
    axis.grid()
    axis.set_ylabel('dB')
    axis.set_xlabel('f (GHz)')
plt.show()
```

### Field profile

To access the field monitor data, we need to first load the simulation dataset from the `TerminalComponentModelerData` instance. The simulation data is stored in a dictionary indexed by keys in the format `<port name>@<mode number>`. For example, to obtain data for the first (and only) excited mode in wave port 1, we use `WP1@0`.

```{python}
#| scrolled: true
# Load monitor data
sim_data = tcm_data.data["WP1@0"]
```

Below, we plot the field magnitude in dB just below the CPW metal plane. 

```{python}
fig, ax = plt.subplots(figsize=(10, 8), tight_layout=True)
sim_data.plot_field(
    "field cpw plane", field_name="E", val="abs", scale="dB", f=f0, ax=ax, vmin=-60, vmax=10
)
ax.set_xlim(-W_cpw / 2, W_cpw / 2)
ax.set_ylim(-L / 2, L / 2)
plt.show()
```

## Conclusion

In this notebook, we used the basic CPW as an example to demonstrate the typical Flex RF workflow for modeling transmission lines. We calculated and benchmarked key quantities such as effective index, attenuation, characteristic impedance, and S-parameters.

In the [next notebook](../cpw-rf-photonics-2/), we will model the CPW in the context of a Mach-Zehnder modulator.


