DMD diffraction tool

I presented in this tutorial the diffraction effects occurring in a DMD setup. It corresponds to a blazed grating effect and depends on the wavelength \(\lambda\), the pixel pitch \(d\), the incident angle \(\alpha\), and the angle of the micro-mirrors \(\theta\). Here is a simple app (see source code on Github) to calculate the criterion for optimal diffraction efficiency and the aspect of the diffraction pattern in the far-field when illuminating the DMD with a plane wave of incident angle \(\alpha\) with respect to the normal of the surface.

Click on "Show widget", it may take a minute to load.


#HIDDEN
# Written by Sebastien Popoff
# 29/10/2016
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
import matplotlib as mpl
from matplotlib import rc
# rc('figure', figsize=(14,8))
from ipywidgets import interact, interact_manual, FloatSlider
#rc('text', usetex=True)
#HIDDEN
def mu_function(theta, d, _lambda):      
    ## A simple criterion matching the diffraction angle and the reflection angle
    _beta = lambda x: (2*theta-x)
    return lambda x: 1.*d/_lambda *(np.sin(x)+np.sin(_beta(x)))
    # when mu is an integer, we are at a blazing angle with a maximum of energy at the order along the optical axis
    # whem mu is n+1/2, the enregy is spread over many diffraction orders not aligned with the optical axis

def get_mu(theta_1D, d, _lambda):

    mu = mu_function(theta_1D, d, _lambda)
    # test different incident angles
    alpha_vec = np.linspace(-75,75,400)*np.pi/180
    alpha_1D_vec = np.arctan(np.tan(alpha_vec)/np.sqrt(2))
    criterion = np.abs(np.mod([mu(a) for a in alpha_1D_vec],1)-0.5)
    
    return alpha_vec, criterion
#HIDDEN
def get_diffraction_pattern(_lambda, d, alpha, theta):

    ## A full numerical simulation of the Fourier plane for an all-on configuration
    beta = 2*theta-alpha
    N_mirrors = 15 # number of mirrors in each direction
    gap = 2 # gap between pixels in micron
    res = 10 # pixels per mirror (for numerical calculation)
    Nx = N_mirrors*res

    ## Pixelate image function
    f = np.ones([N_mirrors,N_mirrors]) # all-on configuration
    ## Phase slope due to incident and reflection angle
    X,Y = np.meshgrid(np.arange(N_mirrors),np.arange(N_mirrors))
    phi = np.exp((X-Y)*complex(0,1)*2*np.pi/_lambda*d*(np.sin(alpha)+np.sin(beta)))
    ## cell unit
    Cell = np.zeros([res,res])
    gpix = int(np.round(gap/(2.*d)*res))
    Cell[gpix:res-gpix,gpix:res-gpix] = 1.
    ## Mirror image
    MI = np.zeros([Nx,Nx],dtype='complex')
    for i in range(N_mirrors):
        for j in range(N_mirrors):
            MI[i*res:(i+1)*res,j*res:(j+1)*res]= f[i,j]*phi[i,j]*Cell
            
    ## In the Fourier plane
    coeff = 5
    FP = np.fft.fftshift(np.fft.fft2(MI,s=[coeff*Nx,coeff*Nx]))
    ROIsize = 300
    ROI = 1
    
    return FP[coeff*Nx//2-ROIsize//2:coeff*Nx//2+ROIsize//2,coeff*Nx//2-ROIsize//2:coeff*Nx//2+ROIsize//2]
    
def make_figure(_lambda, d, alpha, theta):
    
    theta = theta/180*np.pi
    alpha = alpha/180*np.pi
    alpha_1D = np.arctan(np.tan(alpha)/np.sqrt(2))
    theta_1D = np.arctan(np.tan(theta)/np.sqrt(2))
    
    far_field = get_diffraction_pattern(_lambda, d, alpha_1D, theta_1D)
    alpha_vec, mu_vec = get_mu(theta_1D, d, _lambda)
    mu = mu_function(theta_1D, d, _lambda)
    
    f, (a0, a1) = plt.subplots(1, 2, gridspec_kw={'width_ratios': [2, 1]}, figsize = (18,6))
    a0.plot(alpha_vec*180/np.pi,mu_vec,linewidth = 2)
    mu_alpha = np.abs(np.mod(mu(alpha_1D),1)-0.5)
    label = fr' $\alpha = {alpha*180/np.pi:.1f}°$, $\mu = {mu_alpha:.2f}$' 
    a0.plot([alpha*180/np.pi,alpha*180/np.pi],[0.,0.5],label = label,linewidth = 2)
    a0.set_xticks(np.arange(-75,75+15,15))
    a0.set_xticklabels(np.arange(-75,75+15,15),fontsize = 20)
    a0.set_yticks(np.arange(0,0.55,0.1))
    a0.set_yticklabels([f'{x:.1f}' for x in np.arange(0,0.55,0.1)], fontsize = 20)
    a0.set_title(r'Blazing cirterion', fontsize = 25)
    a0.legend(fontsize = 25, loc='lower right')
    a0.set_ylabel(r'Blazing criterion $\mu$', fontsize = 24)
    a0.set_xlabel(r'Incident angle $\alpha$', fontsize = 24)
    
    a1.imshow(
        np.abs(far_field),
        interpolation = 'None',
        clim = [0,(np.max(np.abs(far_field)))/1.5],
        cmap='gray'
    )
    a1.scatter(
        far_field.shape[1]//2, 
        far_field.shape[0]//2, 
        s=400,
        alpha = 0.5,
        c='yellow',
        marker='x'
    )        
    a1.axis('off')    
    a1.set_title('Diffraction pattern', fontsize = 25)
    
    f.tight_layout()
#HIDDEN
theta_slider = FloatSlider(
    min = -20, 
    max = 20, 
    step = 1, 
    value = 12, 
    continuous_update=False, 
    description = r'$\theta$ (degrees)'
)
pitch_slider = FloatSlider(
    min = 5, 
    max = 15, 
    step = 0.1, 
    value = 7.6, 
    continuous_update=False,
    description = r'pitch ($\mu m$)'
)
lambda_slider = FloatSlider(
    min = 0.3, 
    max = 2., 
    step = 0.001, 
    value = 0.632, 
    continuous_update=False,
    description = r'$\lambda$ ($\mu m$)'
)
alpha_slider = FloatSlider(
    min = -75, 
    max = 75, 
    step = 1, 
    value = 24, 
    continuous_update=False, 
    description = r'$\alpha$ (degrees)'
)
interact(make_figure, 
         theta = theta_slider,
         d = pitch_slider,
         alpha = alpha_slider,
         _lambda = lambda_slider);