Skip to content

Accuracy of adjoint gradients in 3D #2757

@oskooi

Description

@oskooi

edit (1/11): updated script with suggestion to disable subpixel smoothing in MaterialGrid (on by default) from @smartalecH which significantly improves accuracy.

This is an initial attempt to verify the accuracy of the adjoint gradients in 3D. Once we are able to get this example working, we can consider converting it into a unit test for #2661 as well as a tutorial example.

The example involves a power splitter (shown in the schematic below) at $\lambda$ = 1.55 μm for a silicon waveguide on a silicon dioxide substrate. This particular SOI waveguide is identical to an existing tutorial with the schematic and band diagram also shown below. The nice thing about the power splitter is that it is an extension of a similar example in 2D and can be modified to use various types of objective arguments (EigenmodeCoefficient, FourierFields, LDOS) and a broad bandwidth objective function. The design region is a 2D grid with the pixels extruded in the $z$ direction. The example uses either a constant or random design region. The entire simulation runs in about 25 minutes using 3 Xeon 4.2 GHz cores.

The test involves the usual comparison of the directional derivative computed using (1) a brute-force finite difference and (2) the adjoint gradient. (This calculation is similar to the 2D simulation of a 1D grating in #2054 (comment).) The MPB output of the simulation shows that the correct waveguide mode is being computed (i.e., matching the values for ($k$, $\omega$) at the red dot in the band diagram below).

At a resolution of 20 pixels/μm, there is a large discrepancy in the finite-difference and adjoint-gradient results:

directional-derivative:, [-0.14618432] (finite difference), [-0.00242355] (adjoint)

This discrepancy does not seem to decrease with increasing resolution. @smartalecH, any thoughts?

power_splitter_snapshot_3

image

"""Validates the adjoint gradient of a power splitter in 3d."""

from autograd import numpy as npa
import meep as mp
import meep.adjoint as mpa
import numpy as np


RESOLUTION = 25  # pixels/μm                                                                                      

WAVELENGTH_UM = 1.55
FREQUENCY_CENTER = 1 / WAVELENGTH_UM

WAVEGUIDE_LENGTH_UM = 3.0
WAVEGUIDE_WIDTH_UM = 0.50
WAVEGUIDE_HEIGHT_UM = 0.22
WAVEGUIDE_SEPARATION_UM = 0.5
SUBSTRATE_UM = 1.0
PADDING_UM = 1.0
PML_UM = 1.0

DESIGN_REGION_SIZE = mp.Vector3(1.6, 1.6)
DESIGN_REGION_RESOLUTION = 2 * RESOLUTION
NX = int(DESIGN_REGION_SIZE.x * DESIGN_REGION_RESOLUTION)
NY = int(DESIGN_REGION_SIZE.y * DESIGN_REGION_RESOLUTION)

SIZE_X = (PML_UM + WAVEGUIDE_LENGTH_UM + DESIGN_REGION_SIZE.x +
          WAVEGUIDE_LENGTH_UM + PML_UM)
SIZE_Y = PML_UM + PADDING_UM + DESIGN_REGION_SIZE.y + PADDING_UM + PML_UM
SIZE_Z = PML_UM + SUBSTRATE_UM + WAVEGUIDE_HEIGHT_UM + PADDING_UM + PML_UM

cell_size = mp.Vector3(SIZE_X, SIZE_Y, SIZE_Z)

pml_layers = [mp.PML(thickness=PML_UM)]

si = mp.Medium(index=3.45)
sio2 = mp.Medium(index=1.45)

eig_parity = mp.ODD_Y

src_pt = mp.Vector3(-0.5 * SIZE_X + PML_UM, 0, 0)
mon_pt = mp.Vector3(
    -0.5 * SIZE_X + PML_UM,
    0.123 * WAVEGUIDE_WIDTH_UM,
    -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM + 0.5 * WAVEGUIDE_HEIGHT_UM
)
refl_pt = mp.Vector3(
    -0.5 * SIZE_X + PML_UM + 0.5 * WAVEGUIDE_LENGTH_UM,
    0,
    0
)
tran_pt_1 = mp.Vector3(
    0.5 * SIZE_X - PML_UM - 0.5 * WAVEGUIDE_LENGTH_UM,
    0.5 * (WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM),
    0
)
tran_pt_2 = mp.Vector3(
    0.5 * SIZE_X - PML_UM - 0.5 * WAVEGUIDE_LENGTH_UM,
    -0.5 * (WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM),
    0
)

stop_cond = mp.stop_when_fields_decayed(50.0, mp.Ez, mon_pt, 1e-6)

def straight_waveguide() -> float:
    """Computes the flux in a straight waveguide for normalization."""

    geometry = [
        # Substrate.                                                                                              
        mp.Block(
            size=mp.Vector3(mp.inf, mp.inf, PML_UM + SUBSTRATE_UM),
            center=mp.Vector3(
                0, 0, -0.5 * SIZE_Z + 0.5 * (PML_UM + SUBSTRATE_UM)
            ),
            material=sio2,
        ),
        # Waveguide.                                                                                              
        mp.Block(
            size=mp.Vector3(mp.inf, WAVEGUIDE_WIDTH_UM, WAVEGUIDE_HEIGHT_UM),
            center=mp.Vector3(
                0,
                0,
                -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM +
                0.5 * WAVEGUIDE_HEIGHT_UM
            ),
            material=si,
        ),
    ]

    sources = [
        mp.EigenModeSource(
            src=mp.GaussianSource(
                FREQUENCY_CENTER, fwidth=0.2 * FREQUENCY_CENTER
            ),
            center=src_pt,
            size=mp.Vector3(0, SIZE_Y, SIZE_Z),
            eig_parity=eig_parity,
        )
    ]

    sim = mp.Simulation(
        resolution=RESOLUTION,
        cell_size=cell_size,
        boundary_layers=pml_layers,
        geometry=geometry,
        sources=sources,
        k_point=mp.Vector3(),
    )

    refl_mon = sim.add_flux(
        FREQUENCY_CENTER,
        0,
        1,
        mp.FluxRegion(center=refl_pt, size=mp.Vector3(0, SIZE_Y, SIZE_Z)),
    )

    sim.run(until_after_sources=stop_cond)

    input_flux = mp.get_fluxes(refl_mon)[0]

    return input_flux


def power_splitter_opt(input_flux: float) -> mpa.OptimizationProblem:
    """Sets up the power-splitter optimization using the adjoint solver."""

    geometry = [
        # Substrate.                                                                                              
        mp.Block(
            size=mp.Vector3(mp.inf, mp.inf, PML_UM + SUBSTRATE_UM),
            center=mp.Vector3(
                0, 0, -0.5 * SIZE_Z + 0.5 * (PML_UM + SUBSTRATE_UM)
            ),
            material=sio2,
        ),
        # Input waveguide.                                                                                        
        mp.Block(
            size=mp.Vector3(
                PML_UM + WAVEGUIDE_LENGTH_UM,
                WAVEGUIDE_WIDTH_UM,
                WAVEGUIDE_HEIGHT_UM
            ),
            center=mp.Vector3(
                -0.5 * SIZE_X + 0.5 * (PML_UM + WAVEGUIDE_LENGTH_UM),
                0,
                -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM +
                0.5 * WAVEGUIDE_HEIGHT_UM
            ),
            material=si,
        ),
        # Output waveguide 1.                                                                                     
        mp.Block(
            size=mp.Vector3(
                WAVEGUIDE_LENGTH_UM + PML_UM,
                WAVEGUIDE_WIDTH_UM,
                WAVEGUIDE_HEIGHT_UM
            ),
            center=mp.Vector3(
                0.5 * SIZE_X - 0.5 * (WAVEGUIDE_LENGTH_UM + PML_UM),
                0.5 * (WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM),
                -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM +
                0.5 * WAVEGUIDE_HEIGHT_UM
            ),
            material=si,
        ),
        # Output waveguide 2.                                                                                     
        mp.Block(
            size=mp.Vector3(
                WAVEGUIDE_LENGTH_UM + PML_UM,
                WAVEGUIDE_WIDTH_UM,
                WAVEGUIDE_HEIGHT_UM
            ),
            center=mp.Vector3(
                0.5 * SIZE_X - 0.5 * (WAVEGUIDE_LENGTH_UM + PML_UM),
                -0.5 * (WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM),
                -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM +
                0.5 * WAVEGUIDE_HEIGHT_UM
            ),
            material=si,
        )
    ]

    matgrid = mp.MaterialGrid(
        mp.Vector3(NX, NY, 1),
        mp.air,
        si,
        weights=np.ones((NX, NY)),
        do_averaging=False,
    )

    matgrid_region = mpa.DesignRegion(
        matgrid,
        volume=mp.Volume(
            center=mp.Vector3(
                0,
                0,
                -0.5 * SIZE_Z + PML_UM + SUBSTRATE_UM +
                0.5 * WAVEGUIDE_HEIGHT_UM
            ),
            size=mp.Vector3(
                DESIGN_REGION_SIZE.x, DESIGN_REGION_SIZE.y, WAVEGUIDE_HEIGHT_UM
            ),
        ),
    )

    matgrid_geometry = [
        mp.Block(
            material=matgrid,
            size=matgrid_region.size,
            center=matgrid_region.center,
        )
    ]

    geometry += matgrid_geometry

    sources = [
        mp.EigenModeSource(
            src=mp.GaussianSource(
                FREQUENCY_CENTER, fwidth=0.2 * FREQUENCY_CENTER
            ),
            center=src_pt,
            size=mp.Vector3(0, SIZE_Y, SIZE_Z),
            eig_parity=eig_parity,
        )
    ]

    sim = mp.Simulation(
        resolution=RESOLUTION,
        cell_size=cell_size,
        boundary_layers=pml_layers,
        geometry=geometry,
        sources=sources,
        k_point=mp.Vector3(),
    )

    obj_list = [
        # Output waveguide 1.                                                                                     
        mpa.EigenmodeCoefficient(
            sim,
            mp.Volume(
                center=tran_pt_1,
                size=mp.Vector3(
                    0, WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM, SIZE_Z
                ),
            ),
            mode=1,
            eig_parity=eig_parity,
        ),
        # Output waveguide 2.                                                                                     
        mpa.EigenmodeCoefficient(
            sim,
            mp.Volume(
                center=tran_pt_2,
                size=mp.Vector3(
                    0, WAVEGUIDE_SEPARATION_UM + WAVEGUIDE_WIDTH_UM, SIZE_Z
                ),
            ),
            mode=1,
            eig_parity=eig_parity,
        ),
    ]

    def obj_func(tran_mon_1, tran_mon_2):
        return (npa.abs(tran_mon_1)**2 + npa.abs(tran_mon_2)**2) / input_flux

    opt = mpa.OptimizationProblem(
        simulation=sim,
        objective_functions=obj_func,
        objective_arguments=obj_list,
        design_regions=[matgrid_region],
        fcen=FREQUENCY_CENTER,
        df=0,
        nf=1,
    )

    return opt

if __name__ == "__main__":
    # input_flux = straight_waveguide()                                                                           

    opt = power_splitter_opt(1.0)

    # Ensure reproducible results.                                                                                
    rng = np.random.RandomState(9861548)

    # Random design region.                                                                                       
    # initial_design_region = 0.9 * rng.rand(NX * NY)                                                             

    # Constant design region.                                                                                     
    initial_design_region = 0.9 * np.ones((NX * NY))

    # Random perturbation for design region.                                                                      
    max_perturbation = 1e-5
    random_perturbation = (max_perturbation *
                           rng.rand(NX * NY))

    unperturbed_val, unperturbed_grad = opt(
        [initial_design_region],
        need_gradient=True
    )

    perturbed_val, _ = opt(
        [initial_design_region + random_perturbation],
        need_gradient=False
    )

    adjoint_directional_deriv = ((random_perturbation[None, :] @
                                  unperturbed_grad).flatten())
    finite_diff_directional_deriv = perturbed_val - unperturbed_val

    print(f"directional-derivative:, {finite_diff_directional_deriv} "
          f"(finite difference), {adjoint_directional_deriv} (adjoint)")

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions