SDKFrameworksAnsysLumericalPlanned

Lumerical FDTD

Integrate OptixLog with Lumerical FDTD for 3D/2D nanophotonic simulations

Lumerical FDTD Integration

Lumerical FDTD is a high-performance 3D/2D Maxwell solver for nanophotonic device design. OptixLog helps you track simulations, compare designs, and collaborate with your team.

Two Integration Methods:

  1. lumapi — Script your simulations in Python (available now)
  2. Companion App — Sync from GUI with one click (coming soon)

lumapi Integration

Basic Example

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


def simulate_waveguide():
    # Initialize OptixLog
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="FDTD_Simulations", create_if_not_exists=True)
    
    # Simulation parameters
    params = {
        "waveguide_width": 0.5,    # μm
        "waveguide_height": 0.22,  # μm
        "wavelength_center": 1.55, # μm
        "wavelength_span": 0.1,    # μm
        "mesh_accuracy": 3,
        "simulation_time": 1000    # fs
    }
    
    run = project.run(
        name="straight_waveguide",
        config={
            "tool": "Lumerical FDTD",
            **params
        }
    )
    
    with lumapi.FDTD() as fdtd:
        # Create simulation region
        fdtd.addfde()
        fdtd.set("x", 0)
        fdtd.set("y", 0)
        fdtd.set("z", 0)
        fdtd.set("x span", 10e-6)
        fdtd.set("y span", 3e-6)
        fdtd.set("z span", 2e-6)
        fdtd.set("mesh accuracy", params["mesh_accuracy"])
        fdtd.set("simulation time", params["simulation_time"] * 1e-15)
        
        # Add waveguide
        fdtd.addrect()
        fdtd.set("name", "waveguide")
        fdtd.set("x", 0)
        fdtd.set("x span", 10e-6)
        fdtd.set("y", 0)
        fdtd.set("y span", params["waveguide_width"] * 1e-6)
        fdtd.set("z", 0)
        fdtd.set("z span", params["waveguide_height"] * 1e-6)
        fdtd.set("material", "Si (Silicon) - Palik")
        
        # Add mode source
        fdtd.addmode()
        fdtd.set("name", "source")
        fdtd.set("injection axis", "x-axis")
        fdtd.set("direction", "Forward")
        fdtd.set("x", -4e-6)
        fdtd.set("y", 0)
        fdtd.set("y span", 2e-6)
        fdtd.set("z", 0)
        fdtd.set("z span", 1.5e-6)
        fdtd.set("wavelength start", (params["wavelength_center"] - params["wavelength_span"]/2) * 1e-6)
        fdtd.set("wavelength stop", (params["wavelength_center"] + params["wavelength_span"]/2) * 1e-6)
        
        # Add transmission monitor
        fdtd.addpower()
        fdtd.set("name", "transmission")
        fdtd.set("monitor type", "2D X-normal")
        fdtd.set("x", 4e-6)
        fdtd.set("y", 0)
        fdtd.set("y span", 2e-6)
        fdtd.set("z", 0)
        fdtd.set("z span", 1.5e-6)
        
        # Log pre-run info
        run.log(step=0, phase="setup_complete")
        
        # Run simulation
        fdtd.run()
        
        # Extract results
        T = fdtd.getresult("transmission", "T")
        wavelengths = T["lambda"].flatten() * 1e9  # Convert to nm
        transmission = T["T"].flatten()
        
        # Log metrics
        run.log(step=1,
                max_transmission=float(np.max(transmission)),
                avg_transmission=float(np.mean(transmission)),
                center_wavelength_nm=params["wavelength_center"] * 1000)
        
        # Create and log plot
        fig, ax = plt.subplots(figsize=(10, 6))
        ax.plot(wavelengths, transmission, 'b-', linewidth=2)
        ax.set_xlabel('Wavelength (nm)')
        ax.set_ylabel('Transmission')
        ax.set_title('Waveguide Transmission Spectrum')
        ax.grid(True, alpha=0.3)
        ax.set_ylim(0, 1.1)
        
        run.log_matplotlib("transmission_spectrum", fig)
        plt.close(fig)
        
        run.log(step=2, simulation_completed=True)
        
    print("✅ FDTD simulation complete!")


if __name__ == "__main__":
    simulate_waveguide()

Common FDTD Workflows

Ring Resonator Analysis

def simulate_ring_resonator():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="RingResonators", create_if_not_exists=True)
    
    params = {
        "ring_radius": 5.0,      # μm
        "waveguide_width": 0.5,  # μm
        "coupling_gap": 0.2,     # μm
    }
    
    run = project.run(
        name=f"ring_r{params['ring_radius']}_gap{params['coupling_gap']}",
        config=params
    )
    
    with lumapi.FDTD() as fdtd:
        fdtd.load("ring_resonator_template.fsp")
        
        # Update parameters
        fdtd.setnamed("ring", "inner radius", (params["ring_radius"] - params["waveguide_width"]/2) * 1e-6)
        fdtd.setnamed("ring", "outer radius", (params["ring_radius"] + params["waveguide_width"]/2) * 1e-6)
        fdtd.setnamed("bus", "y", (params["ring_radius"] + params["coupling_gap"] + params["waveguide_width"]/2) * 1e-6)
        
        fdtd.run()
        
        # Get through and drop transmission
        T_through = fdtd.getresult("through", "T")
        T_drop = fdtd.getresult("drop", "T")
        
        wavelengths = T_through["lambda"].flatten() * 1e9
        through = T_through["T"].flatten()
        drop = T_drop["T"].flatten()
        
        # Find resonances (transmission dips)
        from scipy.signal import find_peaks
        peaks, _ = find_peaks(-through, height=-0.5, distance=10)
        
        resonance_wavelengths = wavelengths[peaks]
        extinction_ratios = through[peaks]
        
        # Log results
        run.log(step=0,
                num_resonances=len(peaks),
                first_resonance_nm=float(resonance_wavelengths[0]) if len(peaks) > 0 else None,
                max_extinction_dB=float(10 * np.log10(min(extinction_ratios))) if len(peaks) > 0 else None)
        
        # Calculate FSR if multiple resonances
        if len(resonance_wavelengths) >= 2:
            fsr = np.diff(resonance_wavelengths)[0]
            run.log(step=1, fsr_nm=float(fsr))
        
        # Plot spectra
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)
        
        ax1.plot(wavelengths, through, 'b-', label='Through')
        ax1.plot(wavelengths, drop, 'r-', label='Drop')
        ax1.set_ylabel('Transmission')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        ax1.set_title('Ring Resonator Spectrum')
        
        ax2.plot(wavelengths, 10*np.log10(through + 1e-10), 'b-', label='Through')
        ax2.plot(wavelengths, 10*np.log10(drop + 1e-10), 'r-', label='Drop')
        ax2.set_xlabel('Wavelength (nm)')
        ax2.set_ylabel('Transmission (dB)')
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        plt.tight_layout()
        run.log_matplotlib("ring_spectrum", fig)
        plt.close(fig)

Field Profile Extraction

def extract_field_profiles():
    with lumapi.FDTD() as fdtd:
        fdtd.load("device.fsp")
        fdtd.run()
        
        # Get E-field data
        E = fdtd.getresult("field_monitor", "E")
        Ex = E["Ex"]
        Ey = E["Ey"]
        Ez = E["Ez"]
        
        # Calculate intensity
        intensity = np.abs(Ex)**2 + np.abs(Ey)**2 + np.abs(Ez)**2
        
        # Get at center wavelength
        wavelengths = E["lambda"].flatten()
        center_idx = len(wavelengths) // 2
        intensity_slice = intensity[:, :, 0, center_idx]  # 2D slice
        
        # Log field statistics
        run.log(step=0,
                max_intensity=float(np.max(intensity_slice)),
                field_confinement=calculate_confinement(intensity_slice))
        
        # Plot
        fig, ax = plt.subplots(figsize=(10, 6))
        im = ax.imshow(intensity_slice.T, cmap='hot', origin='lower')
        ax.set_title(f'E-field Intensity at λ={wavelengths[center_idx]*1e9:.1f} nm')
        plt.colorbar(im, ax=ax, label='|E|²')
        
        run.log_matplotlib("field_profile", fig)
        plt.close(fig)

Parameter Sweeps

Wavelength Sweep

def wavelength_sweep():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="WavelengthSweeps", create_if_not_exists=True)
    
    wavelengths = np.linspace(1.5, 1.6, 11)  # μm
    
    for wl in wavelengths:
        run = project.run(
            name=f"wl_{wl:.3f}um",
            config={"wavelength": wl}
        )
        
        with lumapi.FDTD() as fdtd:
            fdtd.load("device.fsp")
            fdtd.setnamed("source", "wavelength start", wl * 1e-6)
            fdtd.setnamed("source", "wavelength stop", wl * 1e-6)
            fdtd.run()
            
            T = fdtd.getresult("T_monitor", "T")
            run.log(step=0, transmission=float(T["T"][0]))

Geometry Sweep

def geometry_sweep():
    client = Optixlog(api_key=os.getenv("OPTIX_API_KEY"))
    project = client.project(name="GeometrySweeps", create_if_not_exists=True)
    
    widths = [0.4, 0.45, 0.5, 0.55, 0.6]  # μm
    gaps = [0.1, 0.15, 0.2, 0.25, 0.3]    # μm
    
    for width in widths:
        for gap in gaps:
            run = project.run(
                name=f"w{width}_g{gap}",
                config={"width": width, "gap": gap}
            )
            
            with lumapi.FDTD() as fdtd:
                fdtd.load("coupler.fsp")
                fdtd.setnamed("wg1", "y span", width * 1e-6)
                fdtd.setnamed("wg2", "y span", width * 1e-6)
                fdtd.setnamed("wg2", "y", (width + gap) * 1e-6)
                fdtd.run()
                
                coupling = fdtd.getresult("cross", "T")
                run.log(step=0, 
                        coupling_ratio=float(np.max(coupling["T"])),
                        width_um=width,
                        gap_um=gap)

Companion App Integration (Planned)

Coming Soon — The OptixLog Companion App will enable GUI-based workflows without scripting.

How It Will Work

  1. Work normally in FDTD GUI — Design, mesh, simulate
  2. Save your project.fsp file saved to watched directory
  3. Click "Sync" in Companion App — Results uploaded to OptixLog

Planned Configuration

# Companion app will detect FDTD projects
fdtd:
  enabled: true
  extract:
    - monitors: ["transmission", "reflection", "field"]
    - mesh_info: true
    - simulation_time: true
  file_pattern: "*.fsp"

Data Extraction (Planned)

The companion app will automatically extract:

Data TypeExtraction
Transmission spectraFrom power monitors
Reflection spectraFrom power monitors
Field profilesFrom field monitors (optional)
Mesh statisticsCell count, memory
Simulation timeActual runtime
ConvergenceAuto-shutoff status

Tips for FDTD + OptixLog

1. Log Mesh Information

run.log(step=0,
        mesh_cells_x=int(fdtd.getnamed("FDTD", "mesh cells x")),
        mesh_cells_y=int(fdtd.getnamed("FDTD", "mesh cells y")),
        mesh_cells_z=int(fdtd.getnamed("FDTD", "mesh cells z")),
        memory_gb=float(fdtd.getnamed("FDTD", "memory")) / 1024)

2. Track Convergence

# Check if auto-shutoff triggered
if fdtd.getnamed("FDTD", "simulation time") < fdtd.getnamed("FDTD", "stop time"):
    run.log(step=1, early_shutoff=True, 
            final_time_fs=float(fdtd.getnamed("FDTD", "simulation time")) * 1e15)

3. Save Lumerical Files to OptixLog

# Save and log the .fsp file
fdtd.save("simulation.fsp")
run.log_file("simulation_file", "simulation.fsp", "application/octet-stream")

Next Steps

On this page