---
title: Intro to Parameter Sweep
jupyter: python3
---



This notebook demonstrates how to perform parameter sweeps in RF simulations. Individual RF simulations are defined by `TerminalComponentModeler` (TCM) objects. To define a batch job, we simply construct a dictionary of TCM objects where each `(key, value)` pair contains a user-defined text label and the corresponding TCM object. Then, we use the usual `web.run()` command to submit this `dict` object instead of a single TCM.

There are many different methods to generate the dictionary of TCM simulations. For basic parameter sweeps, one can define a parametric wrapper function to build the TCM for a given parameter value set. Then, use a basic `for` loop to iteratively build the dictionary. This workflow is demonstrated below. After the batch simulations are completed, we also demonstrate how to access the S-parameter and field monitor data.

The workflow extends very easily to other design space exploration (DSE) algorithms. Simply replace the basic `for` loop with the appropriate logic for the DSE algorithm.

```{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'
```

Below, we set up some of the basic parameters of this demo. We will be constructing an edge-fed metal patch, and sweeping the width (y length) of the patch.

<center><img src="./img/param_sweep_rf.png" width=540 /></center>

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

# Geometry dimensions
mm = 1000
Lsub, Wsub = (20 * mm, 20 * mm)  # substrate dimensions
Hsub = 1.5 * mm  # substrate thickness
Hmetal = 0.035 * mm  # metal thickness
Lpatch = 8 * mm  # patch dimensions
Lfeed, Wfeed = ((Lsub - Lpatch) / 2, 2.9 * mm)  # feedline size

# Materials
metal = rf.LossyMetalMedium(conductivity=58, frequency_range=(f_min, f_max))
dielectric = rf.Medium(permittivity=4.3)
```

## Defining a Parametric Wrapper Function

Instead of defining a static `TerminalComponentModeler`, we define a wrapper function `build_tcm()` that dynamically generates the TCM for a given `patch_width`.

```{python}
# Parametric function that generates a TerminalComponentModeler (TCM)
def build_tcm(patch_width):
    # Build structures
    substrate = rf.Structure(
        geometry=rf.Box(center=(0, 0, -Hsub / 2), size=(Lsub, Wsub, Hsub)), medium=dielectric
    )
    ground = rf.Structure(
        geometry=rf.Box(center=(0, 0, -Hsub - Hmetal / 2), size=(Lsub, Wsub, Hmetal)), medium=metal
    )
    feed = rf.Structure(
        geometry=rf.Box(
            center=(-Lpatch / 2 - Lfeed / 2, 0, Hmetal / 2), size=(Lfeed, Wfeed, Hmetal)
        ),
        medium=metal,
    )

    # Build patch (note use of input parameter patch_width)
    patch = rf.Structure(
        geometry=rf.Box(center=(0, 0, Hmetal / 2), size=(Lpatch, patch_width, Hmetal)), medium=metal
    )

    # Define port
    LP1 = rf.LumpedPort(
        center=(-Lsub / 2, 0, -Hsub / 2), size=(0, Wfeed, Hsub), voltage_axis=2, name="LP1"
    )

    # Define field monitor
    mon_1 = rf.FieldMonitor(size=(rf.inf, rf.inf, 0), freqs=[f0], name="field (z=0)")

    # Define layer refinement
    lr_options = {
        "min_steps_along_axis": 1,
        "corner_refinement": rf.GridRefinement(dl=0.15 * mm, num_cells=2),
    }
    layer_ref_1 = rf.LayerRefinementSpec.from_structures(structures=[feed, patch], **lr_options)
    layer_ref_2 = rf.LayerRefinementSpec.from_structures(structures=[ground], **lr_options)

    # Define grid
    grid_spec = rf.GridSpec.auto(
        wavelength=rf.C_0 / f0,
        min_steps_per_wvl=20,
        layer_refinement_specs=[layer_ref_1, layer_ref_2],
    )

    # Define simulation and TCM
    sim = rf.Simulation(
        size=(30 * mm, 30 * mm, 8 * mm),
        structures=[substrate, ground, feed, patch],
        monitors=[mon_1],
        grid_spec=grid_spec,
        run_time=5e-9,
        plot_length_units="mm",
    )
    tcm = rf.TerminalComponentModeler(simulation=sim, ports=[LP1], freqs=freqs)

    # Return TCM object
    return tcm
```

## Generate Batch Dictionary

Using `build_tcm()`, we iteratively construct a dictionary `dict_tcm`, whose `(key, value)` pairs are given by a user-defined text label from `sweep_labels` and the respective TCM instance.

```{python}
# Generate list of sweep values and corresponding labels
sweep_values = np.linspace(10 * mm, 12 * mm, 3)
sweep_labels = [f"patch_width={val / mm:.1f}mm" for val in sweep_values]

# Iteratively generate dict for batch job
dict_tcm = {}
for label, val in zip(sweep_labels, sweep_values):
    dict_tcm[label] = build_tcm(patch_width=val)
```

Before running, we can iterate over `dict_tcm` to inspect individual jobs.

```{python}
fig, ax = plt.subplots(1, 3, figsize=(10, 6), tight_layout=True)
for ii, (label, tcm_plot) in enumerate(dict_tcm.items()):
    tcm_plot.plot_sim(z=0, ax=ax[ii], monitor_alpha=0)
    tcm_plot.simulation.plot_grid(z=0, ax=ax[ii])
    ax[ii].set_xlim(-Lsub / 2, Lsub / 2)
    ax[ii].set_ylim(-Wsub / 2, Wsub / 2)
    ax[ii].set_title(label)
plt.show()
```

## Submit Batch Job

We use the `web.run()` method to initiate the job. The syntax for submitting a batch TCM job is identical to that of submitting a single TCM job.

```{python}
# Submit batch job
batch_tcm_data = web.run(dict_tcm, task_name="RF parameter sweep demo", path="./data", verbose=False)
```

## Process Batch Results

After a successful batch run, we use the returned `dict` object `batch_tcm_data` to access the result data. Its structure mirrors that of the dictionary `dict_tcm` that generated it - that is, one can access the individual job data using the same key label `tcm_data = batch_tcm_data[key_label]`. One can also iterate over all batch data using the `batch_tcm_data.items()` iterator, demonstrated below.

```{python}
# Plot S11 comparison
fig, ax = plt.subplots(figsize=(10, 4), tight_layout=True)
for label, tcm_data_iter in batch_tcm_data.items():
    # Get S11 in dB
    S11 = tcm_data_iter.smatrix().data.squeeze()
    S11dB = 20 * np.log10(np.abs(S11))
    # Plot S11
    ax.plot(freqs / 1e9, S11dB, label=label)
ax.legend()
ax.grid()
ax.set_xlabel("f (GHz)")
ax.set_ylabel("S11 (dB)")
plt.show()
```

One can follow a similar process to access `FieldMonitor` data if present in the simulation.

```{python}
# Plot field monitor results
fig, ax = plt.subplots(1, 3, figsize=(10, 3), tight_layout=True)
for ii, (label, tcm_data_iter) in enumerate(batch_tcm_data.items()):
    sim_data = tcm_data_iter.data["LP1"]  # Get simulation data from port 1 excitation
    sim_data.plot_field("field (z=0)", field_name="E", val="abs", ax=ax[ii])
    ax[ii].set_xlim(-Lsub / 2, Lsub / 2)
    ax[ii].set_ylim(-Lsub / 2, Lsub / 2)
    ax[ii].set_title(label)
plt.show()
```


