def main(): # Variables to be set. wavelength = 0.5 * pow(10, -6) pixeltom = 6 * pow(10, -6) distance = 0.1 propagation_type = 'Fraunhofer' k = wavenumber(wavelength) sample_field = np.zeros((150, 150), dtype=np.complex64) sample_field[40:60, 40:60] = 10 sample_field = add_random_phase(sample_field) hologram = propagate_beam(sample_field, k, distance, pixeltom, wavelength, propagation_type) if propagation_type == 'Fraunhofer': distance = np.abs(distance) reconstruction = propagate_beam(hologram, k, distance, pixeltom, wavelength, 'Fraunhofer Inverse') from odak.visualize.plotly import detectorshow detector = detectorshow() detector.add_field(sample_field) detector.show() detector.add_field(hologram) detector.show() detector.add_field(reconstruction / np.amax(np.abs(reconstruction))) detector.show() assert True == True
def adaptive_sampling_angular_spectrum(field,k,distance,dx,wavelength): """ A definition to calculate adaptive sampling angular spectrum based beam propagation. For more Zhang, Wenhui, Hao Zhang, and Guofan Jin. "Adaptive-sampling angular spectrum method with full utilization of space-bandwidth product." Optics Letters 45.16 (2020): 4416-4419. Parameters ---------- field : np.complex Complex field (MxN). k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. distance : float Propagation distance. dx : float Size of one single pixel in the field grid (in meters). wavelength : float Wavelength of the electric field. Returns ======= result : np.complex Final complex field (MxN). """ raise Exception("Adaptive sampling angular spectrum method is not yet stable. See issue https://github.com/kunguz/odak/issues/13") iflag = -1 eps = 10**(-12) nu,nv = field.shape l = nu*dx x = np.linspace(-l/2,l/2,nu) y = np.linspace(-l/2,l/2,nv) X,Y = np.meshgrid(x,y) fx = np.linspace(-1./2./dx,1./2./dx,nu) fy = np.linspace(-1./2./dx,1./2./dx,nv) FX,FY = np.meshgrid(fx,fy) forig = 1./2./dx fc2 = 1./2*(nu/wavelength/np.abs(distance))**0.5 ss = np.abs(fc2)/forig zc = nu*dx**2/wavelength K = nu/2/np.amax(np.abs(fx)) if np.abs(distance) <= zc*2: nnu2 = nu nnv2 = nv fxn = np.linspace(-1./2./dx,1./2./dx,nnu2) fyn = np.linspace(-1./2./dx,1./2./dx,nnv2) else: nnu2 = nu nnv2 = nv fxn = np.linspace(-fc2,fc2,nnu2) fyn = np.linspace(-fc2,fc2,nnv2) FXN,FYN = np.meshgrid(fxn,fxn) Hn = np.exp(1j*k*distance*(1-(FXN*wavelength)**2-(FYN*wavelength)**2)**0.5) FX = FX/np.amax(FX)*np.pi FY = FY/np.amax(FY)*np.pi t_2 = nufft2(field,FX*ss,FY*ss,size=[nnu2,nnv2],sign=iflag,eps=eps) FX = FXN/np.amax(FXN)*np.pi FY = FYN/np.amax(FYN)*np.pi result = nufft2(Hn*t_2,FX*ss,FY*ss,size=[nu,nv],sign=-iflag,eps=eps) return result
def band_extended_angular_spectrum(field,k,distance,dx,wavelength): """ A definition to calculate bandextended angular spectrum based beam propagation. For more Zhang, Wenhui, Hao Zhang, and Guofan Jin. "Band-extended angular spectrum method for accurate diffraction calculation in a wide propagation range." Optics Letters 45.6 (2020): 1543-1546. Parameters ---------- field : np.complex Complex field (MxN). k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. distance : float Propagation distance. dx : float Size of one single pixel in the field grid (in meters). wavelength : float Wavelength of the electric field. Returns ======= result : np.complex Final complex field (MxN). """ iflag = -1 eps = 10**(-12) nv,nu = field.shape l = nu*dx x = np.linspace(-l/2,l/2,nu) y = np.linspace(-l/2,l/2,nv) X,Y = np.meshgrid(x,y) Z = X**2+Y**2 fx = np.linspace(-1./2./dx,1./2./dx,nu) fy = np.linspace(-1./2./dx,1./2./dx,nv) FX,FY = np.meshgrid(fx,fy) K = nu/2/np.amax(fx) fcn = 1./2*(nu/wavelength/np.abs(distance))**0.5 ss = np.abs(fcn)/np.amax(np.abs(fx)) zc = nu*dx**2/wavelength if np.abs(distance) < zc: fxn = fx fyn = fy else: fxn = fx*ss fyn = fy*ss FXN,FYN = np.meshgrid(fxn,fyn) Hn = np.exp(1j*k*distance*(1-(FXN*wavelength)**2-(FYN*wavelength)**2)**0.5) X = X/np.amax(X)*np.pi Y = Y/np.amax(Y)*np.pi t_asmNUFT = nufft2(field,X*ss,Y*ss,sign=iflag,eps=eps) result = nuifft2(Hn*t_asmNUFT,X*ss,Y*ss,sign=-iflag,eps=eps) return result
def fraunhofer_inverse(field,k,distance,dx,wavelength): """ A definition to calculate Inverse Fraunhofer based beam propagation. Parameters ---------- field : np.complex Complex field (MxN). k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. distance : float Propagation distance. dx : float Size of one single pixel in the field grid (in meters). wavelength : float Wavelength of the electric field. Returns ======= result : np.complex Final complex field (MxN). """ distance = np.abs(distance) nv,nu = field.shape l = nu*dx l2 = wavelength*distance/dx dx2 = wavelength*distance/l fx = np.linspace(-l2/2.,l2/2.,nu) fy = np.linspace(-l2/2.,l2/2.,nv) FX,FY = np.meshgrid(fx,fy) FZ = FX**2+FY**2 c = np.exp(1j*k*distance)/(1j*wavelength*distance)*np.exp(1j*k/(2*distance)*FZ) result = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(field/dx**2/c ))) return result
def band_limited_angular_spectrum(field,k,distance,dx,wavelength): """ A definition to calculate bandlimited angular spectrum based beam propagation. For more Matsushima, Kyoji, and Tomoyoshi Shimobaba. "Band-limited angular spectrum method for numerical simulation of free-space propagation in far and near fields." Optics express 17.22 (2009): 19662-19673. Parameters ---------- field : np.complex Complex field (MxN). k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. distance : float Propagation distance. dx : float Size of one single pixel in the field grid (in meters). wavelength : float Wavelength of the electric field. Returns ======= result : np.complex Final complex field (MxN). """ nv,nu = field.shape x = np.linspace(-nu/2*dx,nu/2*dx,nu) y = np.linspace(-nv/2*dx,nv/2*dx,nv) X,Y = np.meshgrid(x,y) Z = X**2+Y**2 h = 1./(1j*wavelength*distance)*np.exp(1j*k*(distance+Z/2/distance)) h = np.fft.fft2(np.fft.fftshift(h))*dx**2 flimx = np.ceil(1/(((2*distance*(1./(nu)))**2+1)**0.5*wavelength)) flimy = np.ceil(1/(((2*distance*(1./(nv)))**2+1)**0.5*wavelength)) mask = np.zeros((nu,nv),dtype=np.complex64) mask = (np.abs(X)<flimx) & (np.abs(Y)<flimy) mask = set_amplitude(h,mask) U1 = np.fft.fft2(np.fft.fftshift(field)) U2 = mask*U1 result = np.fft.ifftshift(np.fft.ifft2(U2)) return result
def calculate_intensity(field): """ Definition to calculate intensity of a single or multiple given electric field(s). Parameters ---------- field : ndarray.complex or complex Electric fields or an electric field. Returns ---------- intensity : float Intensity or intensities of electric field(s). """ intensity = np.abs(field)**2 return intensity
def calculate_amplitude(field): """ Definition to calculate amplitude of a single or multiple given electric field(s). Parameters ---------- field : ndarray.complex or complex Electric fields or an electric field. Returns ---------- amplitude : float Amplitude or amplitudes of electric field(s). """ amplitude = np.abs(field) return amplitude
def intersect_parametric(ray, parametric_surface, surface_function, surface_normal_function, target_error=0.00000001, iter_no_limit=100000): """ Definition to intersect a ray with a parametric surface. Parameters ---------- ray : ndarray Ray. parametric_surface : ndarray Parameters of the surfaces. surface_function : function Function to evaluate a point against a surface. surface_normal_function : function Function to calculate surface normal for a given point on a surface. target_error : float Target error that defines the precision. iter_no_limit : int Maximum number of iterations. Returns ---------- distance : float Propagation distance. normal : ndarray Ray that defines a surface normal for the intersection. """ if len(ray.shape) == 2: ray = ray.reshape((1, 2, 3)) error = [150, 100] distance = [0, 0.1] iter_no = 0 while np.abs(np.max(np.asarray(error[1]))) > target_error: error[1], point = intersection_kernel_for_parametric_surfaces( distance[1], ray, parametric_surface, surface_function) distance, error = propagate_parametric_intersection_error( distance, error) iter_no += 1 if iter_no > iter_no_limit: return False, False if np.isnan(np.sum(point)): return False, False normal = surface_normal_function(point, parametric_surface) return distance[1], normal
def main(): # Variables to be set. wavelength = 0.5*pow(10,-6) pixeltom = 6*pow(10,-6) distance = 10.0 propagation_type = 'Fraunhofer' k = wavenumber(wavelength) sample_field = np.zeros((150,150),dtype=np.complex64) sample_field = np.zeros((150,150),dtype=np.complex64) sample_field[ 65:85, 65:85 ] = 1 sample_field = add_random_phase(sample_field) hologram = propagate_beam( sample_field, k, distance, pixeltom, wavelength, propagation_type ) if propagation_type == 'Fraunhofer': # Uncomment if you want to match the physical size of hologram and input field. #from odak.wave import fraunhofer_equal_size_adjust #hologram = fraunhofer_equal_size_adjust(hologram,distance,pixeltom,wavelength) propagation_type = 'Fraunhofer Inverse' distance = np.abs(distance) reconstruction = propagate_beam( hologram, k, -distance, pixeltom, wavelength, propagation_type ) #from odak.visualize.plotly import detectorshow #detector = detectorshow() #detector.add_field(sample_field) #detector.show() #detector = detectorshow() #detector.add_field(hologram) #detector.show() #detector = detectorshow() #detector.add_field(reconstruction) #detector.show() assert True==True
def intersect_w_surface(ray, points): """ Definition to find intersection point inbetween a surface and a ray. For more see: http://geomalgorithms.com/a06-_intersect-2.html Parameters ---------- ray : ndarray A vector/ray. points : ndarray Set of points in X,Y and Z to define a planar surface. Returns ---------- normal : ndarray Surface normal at the point of intersection. distance : float Distance in between starting point of a ray with it's intersection with a planar surface. """ points = np.asarray(points) normal = get_triangle_normal(points) if len(ray.shape) == 2: ray = ray.reshape((1, 2, 3)) if len(points) == 2: points = points.reshape((1, 3, 3)) if len(normal.shape) == 2: normal = normal.reshape((1, 2, 3)) f = normal[:, 0] - ray[:, 0] distance = np.dot(normal[:, 1], f.T) / np.dot(normal[:, 1], ray[:, 1].T) n = np.int(np.amax(np.array([ray.shape[0], normal.shape[0]]))) normal = np.zeros((n, 2, 3)) normal[:, 0] = ray[:, 0] + distance.T * ray[:, 1] distance = np.abs(distance) if normal.shape[0] == 1: normal = normal.reshape((2, 3)) distance = distance.reshape((1)) if distance.shape[0] == 1 and len(distance.shape) > 1: distance = distance.reshape((distance.shape[1])) return normal, distance
def rayleigh_sommerfeld(field,k,distance,dx,wavelength): """ Definition to compute beam propagation using Rayleigh-Sommerfeld's diffraction formula (Huygens-Fresnel Principle). For more see Section 3.5.2 in Goodman, Joseph W. Introduction to Fourier optics. Roberts and Company Publishers, 2005. Parameters ---------- field : np.complex Complex field (MxN). k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. distance : float Propagation distance. dx : float Size of one single pixel in the field grid (in meters). wavelength : float Wavelength of the electric field. Returns ======= result : np.complex Final complex field (MxN). """ nv,nu = field.shape x = np.linspace(-nv*dx/2,nv*dx/2,nv) y = np.linspace(-nu*dx/2,nu*dx/2,nu) X,Y = np.meshgrid(x,y) Z = X**2+Y**2 result = np.zeros(field.shape,dtype=np.complex64) direction = int(distance/np.abs(distance)) for i in range(nu): for j in range(nv): if field[i,j] != 0: r01 = np.sqrt(distance**2+(X-X[i,j])**2+(Y-Y[i,j])**2)*direction cosnr01 = np.cos(distance/r01) result += field[i,j]*np.exp(1j*k*r01)/r01*cosnr01 result *= 1./(1j*wavelength) return result
def propagate_parametric_intersection_error(distance, error): """ Definition to propagate the error in parametric intersection to find the next distance to try. Parameters ---------- distance : list List that contains the new and the old distance. error : list List that contains the new and the old error. Returns ---------- distance : list New distance. error : list New error. """ new_distance = distance[1] - error[1] * (distance[1] - distance[0]) / ( error[1] - error[0]) distance[0] = distance[1] distance[1] = np.abs(new_distance) error[0] = error[1] return distance, error
def point_wise(field,distances,k,dx,wavelength,lens_method='ideal',propagation_method='Bandlimited Angular Spectrum',n_iteration=3): """ Point-wise hologram calculation method. For more Maimone, Andrew, Andreas Georgiou, and Joel S. Kollin. "Holographic near-eye displays for virtual and augmented reality." ACM Transactions on Graphics (TOG) 36.4 (2017): 1-16. Parameters ---------- field : ndarray Complex input field to be converted into a hologram. distances : ndarray Depth map of the input field. k : odak.wave.wavenumber Wave number of a wave, see odak.wave.wavenumber for more. dx : float Pixel pitch. wavelength : float Wavelength of the light. lens_model : str Method to calculate the lens patterns. propagation_mode : str Beam propagation method to be used if the lens_model is not equal to `ideal`. n_iteration : int Number of iterations. Returns ---------- hologram : ndarray Generated complex hologram. """ hologram = np.zeros(field.shape,dtype=np.complex64) nx,ny = field.shape cx = int(nx/2) cy = int(ny/2) non_zeros = np.asarray((np.abs(field)>0).nonzero()) unique_dist = np.unique(distances) unique_dist = unique_dist[unique_dist!=0] target = np.zeros((nx,ny),dtype=np.complex64) target[cx,cy] = 1. lenses = [] for distance in unique_dist: if lens_method == 'ideal': new_lens = quadratic_phase_function(nx,ny,k,focal=distance,dx=dx) lenses.append(new_lens) elif lens_method == 'Gerchberg-Saxton': new_lens,_ = gerchberg_saxton( target, n_iteration, distance, dx, wavelength, np.pi*2, propagation_method ) lenses.append(new_lens) for m in tqdm(range(non_zeros.shape[1])): i = int(non_zeros[0,m]) j = int(non_zeros[1,m]) lens_id = int(np.argwhere(unique_dist==distances[i,j])) lens = lenses[lens_id] lens = np.roll(lens,i-cx,axis=0) lens = np.roll(lens,j-cy,axis=1) hologram += lens*field[i,j] return hologram