ExamplesMeep Examples

Metasurface Lens

Design and simulate a binary-grating metasurface lens for focusing light

Metasurface Lens Design

This example demonstrates the design and simulation of a binary-grating metasurface lens that focuses light by engineering the phase profile across the aperture.

Application: Metasurface lenses (metalenses) enable flat, compact optical elements for imaging, displays, and telecommunications.


Physics Background

Phase Engineering

A lens focuses light by imparting a position-dependent phase:

$$ \phi(x) = \frac{2\pi}{\lambda}\left(f - \sqrt{x^2 + f^2}\right) $$

where f is the focal length and x is the transverse position.

Binary Grating Unit Cell

By varying the duty cycle of a binary grating, we can control:

  • Transmittance — Power transmitted through each cell
  • Phase — Accumulated phase shift

Simulation Strategy

Phase Map Characterization

Simulate unit cells with varying duty cycles to map phase vs. duty cycle.

Phase Profile Design

Calculate required phase at each position for desired focal length.

Lens Assembly

Select appropriate duty cycle for each grating cell.

Far-Field Focusing

Simulate complete lens and compute far-field intensity distribution.


Configuration Parameters

ParameterValueDescription
resolution50Pixels per μm
gp0.3Grating period (μm)
gh1.8Grating height (μm)
focal_length200Target focal length (μm)
wavelength0.5Center wavelength (μm)

Complete Code

"""
Metasurface Lens Simulation with OptixLog Integration

Simulates a binary-grating metasurface lens and analyzes its focusing properties.
"""

import os
import meep as mp
import matplotlib
matplotlib.use("agg")
import matplotlib.pyplot as plt
import numpy as np
from optixlog import Optixlog


# Global parameters
RESOLUTION = 50
DPML = 1.0
DSUB = 2.0
DPAD = 2.0
LCEN = 0.5  # center wavelength
FCEN = 1 / LCEN
DF = 0.2 * FCEN
FOCAL_LENGTH = 200
SPOT_LENGTH = 100
FF_RES = 10

GLASS = mp.Medium(index=1.5)
K_POINT = mp.Vector3(0, 0, 0)
PML_LAYERS = [mp.PML(thickness=DPML, direction=mp.X)]
SYMMETRIES = [mp.Mirror(mp.Y)]


def grating_unit_cell(gp, gh, gdc):
    """Simulate single grating unit cell to get phase and transmittance"""
    sx = DPML + DSUB + gh + DPAD + DPML
    sy = gp
    cell_size = mp.Vector3(sx, sy, 0)
    
    src_pt = mp.Vector3(-0.5 * sx + DPML + 0.5 * DSUB)
    mon_pt = mp.Vector3(0.5 * sx - DPML - 0.5 * DPAD)
    
    sources = [
        mp.Source(
            mp.GaussianSource(FCEN, fwidth=DF),
            component=mp.Ez,
            center=src_pt,
            size=mp.Vector3(y=sy),
        )
    ]
    
    # Empty cell for normalization
    sim = mp.Simulation(
        resolution=RESOLUTION,
        cell_size=cell_size,
        boundary_layers=PML_LAYERS,
        k_point=K_POINT,
        default_material=GLASS,
        sources=sources,
        symmetries=SYMMETRIES,
    )
    
    flux_obj = sim.add_flux(FCEN, 0, 1, mp.FluxRegion(center=mon_pt, size=mp.Vector3(y=sy)))
    sim.run(until_after_sources=50)
    input_flux = mp.get_fluxes(flux_obj)
    sim.reset_meep()
    
    # With grating
    geometry = [
        mp.Block(
            material=GLASS,
            size=mp.Vector3(DPML + DSUB, mp.inf, mp.inf),
            center=mp.Vector3(-0.5 * sx + 0.5 * (DPML + DSUB)),
        ),
        mp.Block(
            material=GLASS,
            size=mp.Vector3(gh, gdc * gp, mp.inf),
            center=mp.Vector3(-0.5 * sx + DPML + DSUB + 0.5 * gh),
        ),
    ]
    
    sim = mp.Simulation(
        resolution=RESOLUTION,
        cell_size=cell_size,
        boundary_layers=PML_LAYERS,
        geometry=geometry,
        k_point=K_POINT,
        sources=sources,
        symmetries=SYMMETRIES,
    )
    
    flux_obj = sim.add_flux(FCEN, 0, 1, mp.FluxRegion(center=mon_pt, size=mp.Vector3(y=sy)))
    sim.run(until_after_sources=200)
    
    res = sim.get_eigenmode_coefficients(flux_obj, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y)
    coeffs = res.alpha
    
    mode_tran = abs(coeffs[0, 0, 0]) ** 2 / input_flux[0]
    mode_phase = np.angle(coeffs[0, 0, 0])
    if mode_phase > 0:
        mode_phase -= 2 * np.pi
    
    return mode_tran, mode_phase


def main():
    api_key = os.getenv("OPTIX_API_KEY", "your_api_key_here")
    
    client = Optixlog(api_key=api_key)
    project = client.project(name="MeepExamples", create_if_not_exists=True)

    gp = 0.3  # periodicity
    gh = 1.8  # height
    gdc = np.linspace(0.1, 0.9, 30)
    
    run = project.run(
        name="metasurface_lens_design",
        config={
            "simulation_type": "metasurface_lens",
            "grating_period": gp,
            "grating_height": gh,
            "focal_length": FOCAL_LENGTH,
            "wavelength": LCEN,
            "resolution": RESOLUTION
        }
    )
    
    run.log(step=0, grating_period=gp, grating_height=gh, focal_length=FOCAL_LENGTH)

    # Phase map calculation
    print("⚡ Computing phase map...")
    mode_tran = np.empty(gdc.size)
    mode_phase = np.empty(gdc.size)
    
    for n in range(gdc.size):
        mode_tran[n], mode_phase[n] = grating_unit_cell(gp, gh, gdc[n])
        if n % 5 == 0:
            print(f"   Computed {n+1}/{gdc.size} duty cycles")

    phase_range = mode_phase.max() - mode_phase.min()
    
    run.log(step=1, phase="phase_map_completed",
            phase_range=float(phase_range),
            min_transmittance=float(np.min(mode_tran)),
            max_transmittance=float(np.max(mode_tran)))

    # Create phase map plot
    fig1, axes = plt.subplots(1, 2, figsize=(12, 5))
    
    axes[0].plot(gdc, mode_tran, 'bo-', markersize=4)
    axes[0].set_xlabel("Grating Duty Cycle")
    axes[0].set_ylabel("Transmittance")
    axes[0].set_title("Transmittance vs Duty Cycle")
    axes[0].grid(True, alpha=0.3)
    
    axes[1].plot(gdc, mode_phase, 'rs-', markersize=4)
    axes[1].set_xlabel("Grating Duty Cycle")
    axes[1].set_ylabel("Phase (radians)")
    axes[1].set_title("Phase vs Duty Cycle")
    axes[1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    run.log_matplotlib("phase_map", fig1)
    plt.close(fig1)
    print(f"   Phase range: {phase_range:.2f} radians")

    # Log summary
    run.log(step=2,
            phase_range_radians=phase_range,
            simulation_completed=True)
    
    print(f"\n✅ Phase map complete!")
    print(f"   Use this map to design metasurface lenses")


if __name__ == "__main__":
    main()

Key Concepts

Unit Cell Characterization

Extract phase and transmittance for each duty cycle:

res = sim.get_eigenmode_coefficients(flux_obj, [1], eig_parity=mp.ODD_Z + mp.EVEN_Y)
mode_tran = abs(res.alpha[0, 0, 0]) ** 2 / input_flux[0]
mode_phase = np.angle(res.alpha[0, 0, 0])

Phase Unwrapping

Ensure continuous phase values:

if mode_phase > 0:
    mode_phase -= 2 * np.pi

Lens Phase Profile

Required phase for focusing:

phase_required = 2 * np.pi / wavelength * (f - np.sqrt(x**2 + f**2))

Expected Results

Phase Map

  • Phase varies monotonically with duty cycle
  • Total phase range ~π to 2π (depending on height)
  • Transmittance varies but typically > 50%

Focusing

With sufficient phase range:

  • Focal spot appears at designed distance
  • Spot size limited by diffraction (lens aperture)

Design Guidelines

Phase RangeLens Quality
< πPoor, multiple foci
π - 1.5πModerate, some aberrations
> 1.5πGood, clean focusing
Ideal, full control

OptixLog Integration

Logged Metrics

MetricDescription
grating_periodUnit cell period
grating_heightRidge height
phase_rangeTotal achievable phase
focal_lengthTarget focus distance

Logged Plots

  • phase_map — Transmittance and phase vs duty cycle

Further Reading

On this page