Adaptive Scan

Highlighted features: sweep(adaptive=True) for a fine wavelength spectrum from a sparse set of solves, and the tolerance that sets how many.

This code example is licensed under the BSD 3-Clause License.

  • Python
import sys
import emodeconnection as emc
import numpy as np
from matplotlib import pyplot as plt

## Adaptive wavelength scanning: report a fine spectrum from a few solves.
## The scan places solves only where the mode list is still moving and
## interpolates the rest, so the cost follows how fast the modes change rather
## than how many points are asked for.

## Set simulation parameters
dx, dy = 20, 20  # [nm] resolution
w_core, h_core = 800, 220  # [nm] Si core
clearance = 1500  # [nm] cladding on each side
num_modes = 2  # [-] both guided across the band below

wav_lo, wav_hi = 1500, 1600  # [nm]
wavelengths = np.linspace(wav_lo, wav_hi, 201)  # [nm] where results are wanted

## Connect and initialize EMode
em = emc.EMode(emode_cmd=sys.argv[1:], simulation_name='adaptive_scan')

em.settings(
    wavelength=(wav_lo + wav_hi) / 2, x_resolution=dx, y_resolution=dy,
    window_width=w_core + 2 * clearance, window_height=h_core + 2 * clearance,
    num_modes=num_modes, boundary_condition='SS', background_material='SiO2')

em.shape(name='clad', material='SiO2', height=clearance)
em.shape(name='core', material='Si', width=w_core, height=h_core)

## How many solves the scan costs is set by `tolerance`, the largest mode-list
## change tolerated between neighbouring solves -- not by the number of points.
for tolerance in (1e-2, 1e-3, 5e-4):
    scan = em.sweep(key='wavelength', values=wavelengths, adaptive=True,
                    tolerance=tolerance, result=['effective_index'])
    n_solves = len(scan['solved_wavelengths'])
    print(f'tolerance {tolerance:.0e}: {n_solves:3d} solves for {len(wavelengths)} points'
          f'  ({len(wavelengths) / n_solves:.0f}x fewer)')
    if scan['unconverged']:
        print(f'  unresolved intervals: {len(scan["unconverged"])}')

## The last scan above is the one kept. Mark where it actually solved.
solved = np.sort(np.asarray(scan['solved_wavelengths']))
n_eff = np.real(np.asarray(scan['effective_index']))

em.settings(wavelength=1550)
em.FDM()
em.plot(component='Ex', mode=0)

em.close()

plt.figure(figsize=(6, 4.5))
for m in range(num_modes):
    line, = plt.plot(scan['values'], n_eff[m], label=f'mode {m}')
    ## Bisection places the solves, so they are not generally grid points;
    ## mark them on the interpolant at the nearest reported wavelength.
    picked = np.clip(np.searchsorted(scan['values'], solved), 0, len(scan['values']) - 1)
    plt.plot(scan['values'][picked], n_eff[m][picked], 'o', ms=5,
             color=line.get_color(), markerfacecolor='none')
plt.xlabel('Wavelength (nm)')
plt.ylabel('Effective index')
plt.title(f'Adaptive scan: {len(solved)} solves, {len(scan["values"])} points reported')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('adaptive_scan.png', dpi=300, bbox_inches='tight')

## Interpolation between solves is linear, so a scan is for reading values off,
## not for differentiating twice: a dispersion curve wants solves at every point.

Console output:

EMode3D 1.0.4 - email
Adaptive wavelength scan over 1500-1600 nm... completed in 5.5 sec
Adaptive wavelength scan over 1500-1600 nm... completed in 12.3 sec
Adaptive wavelength scan over 1500-1600 nm... completed in 29.6 sec
Meshing completed in 0.4 sec
Solving modes completed in 0.7 sec
Exited EMode
tolerance 1e-02:   5 solves for 201 points  (40x fewer)
tolerance 1e-03:  12 solves for 201 points  (17x fewer)
tolerance 5e-04:  28 solves for 201 points  (7x fewer)

Figures:

../_images/adaptive_scan.png
../_images/adaptive_Ex.png