├── .gitignore ├── LICENSE ├── examples ├── reimage.py ├── schiempflug.py ├── objective.py ├── relay.py ├── concave_mirror.py ├── grating.py ├── field_lens.py └── dmd.py ├── README.md └── modules ├── ray_utilities.py ├── lf_4d.py ├── visualize.py ├── lf_2d.py └── raytracing.py /.gitignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | __pycache__/ 3 | modules/__pycache__/ 4 | systems/ 5 | 6 | *.pyc 7 | *.bmp 8 | *.png 9 | 10 | *.swp 11 | *.swo 12 | *~ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vishwanath S R V 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/reimage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | 12 | if __name__ == '__main__': 13 | # Create simple system to reimage using lens 14 | components = [] 15 | rays = [] 16 | image_plane = -120 17 | 18 | # System contains just of one lens 19 | components.append(rt.Lens(f=100, 20 | aperture=100, 21 | pos=[0,0], 22 | theta=0)) 23 | 24 | # Create three points and three rays from each point 25 | rays.append([image_plane, 10, -np.pi/20]) 26 | rays.append([image_plane, 10, 0]) 27 | rays.append([image_plane, 10, np.pi/20]) 28 | 29 | rays.append([image_plane, 0, -np.pi/20]) 30 | rays.append([image_plane, 0, 0]) 31 | rays.append([image_plane, 0, np.pi/20]) 32 | 33 | rays.append([image_plane, -10, -np.pi/20]) 34 | rays.append([image_plane, -10, 0]) 35 | rays.append([image_plane, -10, np.pi/20]) 36 | 37 | colors = 'rrrgggbbb' 38 | 39 | # Propagate the rays 40 | ray_bundles = rt.propagate_rays(components, rays) 41 | 42 | # Create a new canvas 43 | canvas = vis.Canvas([-200, 600], [-100, 100]) 44 | 45 | # Draw the components 46 | canvas.draw_components(components) 47 | 48 | # Draw the rays 49 | canvas.draw_rays(ray_bundles, colors) 50 | 51 | # Show the system 52 | canvas.show() 53 | 54 | # Save a copy 55 | canvas.save('reimage.png') 56 | -------------------------------------------------------------------------------- /examples/schiempflug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | 12 | if __name__ == '__main__': 13 | # Create simple system to reimage using lens with scheimpflug 14 | components = [] 15 | rays = [] 16 | image_plane1 = -150 17 | image_plane2 = -140 18 | image_plane3 = -130 19 | 20 | # System contains just of one lens 21 | components.append(rt.Lens(f=100, 22 | aperture=100, 23 | pos=[0,0], 24 | theta=0)) 25 | 26 | # Create three points and three rays from each point 27 | rays.append([image_plane1, 10, -np.pi/20]) 28 | rays.append([image_plane1, 10, 0]) 29 | rays.append([image_plane1, 10, np.pi/20]) 30 | 31 | rays.append([image_plane2, 0, -np.pi/20]) 32 | rays.append([image_plane2, 0, 0]) 33 | rays.append([image_plane2, 0, np.pi/20]) 34 | 35 | rays.append([image_plane3, -10, -np.pi/20]) 36 | rays.append([image_plane3, -10, 0]) 37 | rays.append([image_plane3, -10, np.pi/20]) 38 | 39 | colors = 'rrrgggbbb' 40 | 41 | # Propagate the rays 42 | ray_bundles = rt.propagate_rays(components, rays) 43 | 44 | # Create a new canvas 45 | canvas = vis.Canvas([-200, 600], [-100, 100]) 46 | 47 | # Draw the components 48 | canvas.draw_components(components) 49 | 50 | # Draw the rays 51 | canvas.draw_rays(ray_bundles, colors) 52 | 53 | # Show the system 54 | canvas.show() 55 | 56 | # Save a copy 57 | canvas.save('schiempflug.png') 58 | -------------------------------------------------------------------------------- /examples/objective.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | import ray_utilities 12 | 13 | if __name__ == '__main__': 14 | # Create a relay lens system 15 | components = [] 16 | rays = [] 17 | image_plane = -300 18 | nrays = 10 19 | 20 | # Objective is simulated using two lenses 21 | components.append(rt.Lens(f=30, 22 | aperture=100, 23 | pos=[0,0], 24 | theta=0)) 25 | 26 | # Second lens creates the flange focal distance 27 | components.append(rt.Lens(f=13, 28 | aperture=50, 29 | pos=[20,0], 30 | theta=0)) 31 | 32 | # Create three points and three rays from each point 33 | rays += ray_utilities.ray_fan([image_plane, 200], [-np.pi/5, -np.pi/6], nrays) 34 | rays += ray_utilities.ray_fan([image_plane, 0], [-np.pi/30, np.pi/30], nrays) 35 | rays += ray_utilities.ray_fan([image_plane, -200], [np.pi/6, np.pi/5], nrays) 36 | 37 | colors = 'r'*nrays + 'g'*nrays + 'b'*nrays 38 | 39 | # Propagate the rays 40 | ray_bundles = rt.propagate_rays(components, rays) 41 | 42 | # Create a new canvas 43 | canvas = vis.Canvas([-300, 100], [-200, 200]) 44 | 45 | # Draw the components 46 | canvas.draw_components(components) 47 | 48 | # Draw the rays 49 | canvas.draw_rays(ray_bundles, colors) 50 | 51 | # Show the system 52 | canvas.show() 53 | 54 | # Save a copy 55 | canvas.save('objective.png') 56 | -------------------------------------------------------------------------------- /examples/relay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | 12 | if __name__ == '__main__': 13 | # Create a relay lens system 14 | components = [] 15 | rays = [] 16 | image_plane = -100 17 | 18 | # System contains two lenses 19 | components.append(rt.Lens(f=100, 20 | aperture=100, 21 | pos=[0,0], 22 | theta=0)) 23 | 24 | components.append(rt.Lens(f=100, 25 | aperture=100, 26 | pos=[30,0], 27 | theta=0)) 28 | 29 | # Create three points and three rays from each point 30 | rays.append([image_plane, 10, -np.pi/20]) 31 | rays.append([image_plane, 10, 0]) 32 | rays.append([image_plane, 10, np.pi/20]) 33 | 34 | rays.append([image_plane, 0, -np.pi/20]) 35 | rays.append([image_plane, 0, 0]) 36 | rays.append([image_plane, 0, np.pi/20]) 37 | 38 | rays.append([image_plane, -10, -np.pi/20]) 39 | rays.append([image_plane, -10, 0]) 40 | rays.append([image_plane, -10, np.pi/20]) 41 | 42 | colors = ['c', 'c', 'c', 'm', 'm', 'm', 'y', 'y', 'y'] 43 | 44 | # Propagate the rays 45 | ray_bundles = rt.propagate_rays(components, rays) 46 | 47 | # Create a new canvas 48 | canvas = vis.Canvas([-200, 200], [-100, 100]) 49 | 50 | # Draw the components 51 | canvas.draw_components(components) 52 | 53 | # Draw the rays 54 | canvas.draw_rays(ray_bundles, colors) 55 | 56 | # Show the system 57 | canvas.show() 58 | 59 | # Save a copy 60 | canvas.save('relay.png') 61 | -------------------------------------------------------------------------------- /examples/concave_mirror.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | sys.path.append('../modules') 6 | 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | 10 | import raytracing as rt 11 | import visualize as vis 12 | import ray_utilities 13 | 14 | if __name__ == '__main__': 15 | # Constants 16 | image_plane = -1e6 # Image plane from first lens 17 | fs = 100 # System focal length 18 | aperture = 25.4 # Diameter of each mirror 19 | npoints = 3 # Number of scene points 20 | ymax = 1e5 # Size of imaging area 21 | ymin = -1e5 22 | nrays = 10 # Number of rays per scene point 23 | lmb = 500 # Design wavelength 24 | 25 | # Create a scene 26 | scene = np.zeros((2, npoints)) 27 | scene[0, :] = image_plane 28 | scene[1, :] = np.linspace(ymin, ymax, npoints) 29 | 30 | components = [] 31 | 32 | # Add a concave mirror 33 | components.append(rt.SphericalMirror(f=fs, 34 | aperture=aperture, 35 | pos=[0, 0], 36 | theta=0)) 37 | 38 | # Place a detector just on the focal plane of the mirror 39 | components.append(rt.Sensor(aperture=aperture, 40 | pos=[-fs, 0], 41 | theta=np.pi)) 42 | 43 | # Get initial rays 44 | [rays, ptdict, colors] = ray_utilities.initial_rays(scene, 45 | components[0], 46 | nrays) 47 | # Color code the rays emanating from each scene point 48 | colors = ['b']*nrays + ['g']*nrays + ['r']*nrays 49 | 50 | # Create a new canvas 51 | canvas = vis.Canvas(xlim=[-2*fs, 5], 52 | ylim=[-aperture, aperture], 53 | figsize=[12, 12]) 54 | 55 | ray_bundles = rt.propagate_rays(components, rays, lmb) 56 | canvas.draw_rays(ray_bundles, colors) 57 | 58 | # Draw the components 59 | canvas.draw_components(components) 60 | 61 | # Show the canvas 62 | canvas.show() 63 | -------------------------------------------------------------------------------- /examples/grating.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | import ray_utilities 12 | 13 | if __name__ == '__main__': 14 | # Create a spectrometer using a simple 4f system and diffraction grating 15 | f = 50 # Focal length of all lenses 16 | aperture = 25.4 # Size of lenses 17 | npoints = 3 # Number of light source points 18 | nrays = 5 # Number of light rays per point 19 | ymax = -0.1 # Limit of source plane. Controls spectral resolution 20 | ymin = 0.1 21 | ngroves = 600 # Grove density of diffraction grating 22 | 23 | # Simulate system for these wavelengths 24 | lmb = list(np.linspace(400, 700, 11)*1e-9) 25 | 26 | components = [] 27 | rays = [] 28 | image_plane = -200 29 | nrays = 20 30 | 31 | # Create three scene points 32 | scene = np.zeros((2, npoints)) 33 | scene[1, :] = np.linspace(ymin, ymax, npoints) 34 | 35 | # Place a collimation lens 36 | components.append(rt.Lens(f=f, 37 | aperture=aperture, 38 | pos=[f, 0], 39 | theta=0)) 40 | 41 | # Place a diffraction grating 42 | components.append(rt.Grating(ngroves=ngroves, 43 | aperture=aperture, 44 | pos=[2*f, 0], 45 | theta=0)) 46 | 47 | # Place a lens such that the central wavelength is centered on the sensor 48 | theta_design = np.arcsin(lmb[len(lmb)//2]/(1e-3/ngroves)) 49 | x1 = 2*f + f*np.cos(-theta_design) 50 | y1 = f*np.sin(-theta_design) 51 | 52 | components.append(rt.Lens(f=f, 53 | aperture=aperture, 54 | pos=[x1, y1], 55 | theta=theta_design)) 56 | 57 | # Place a sensor 58 | x2 = x1 + f*np.cos(-theta_design) 59 | y2 = y1 + f*np.sin(-theta_design) 60 | 61 | components.append(rt.Sensor(aperture=aperture, 62 | pos=[x2, y2], 63 | theta=theta_design)) 64 | 65 | # Get the initial rays 66 | [rays, ptdict, colors] = ray_utilities.initial_rays(scene, 67 | components[0], 68 | nrays) 69 | # Create rainbow colors 70 | colors = vis.get_colors(len(lmb), nrays*npoints, cmap='rainbow') 71 | 72 | # Create a new canvas 73 | canvas = vis.Canvas([-5, 4.1*f], [-2*aperture, 2*aperture]) 74 | 75 | # Draw the components 76 | canvas.draw_components(components) 77 | 78 | # Draw the rays for each wavelength 79 | for idx in range(len(lmb)): 80 | canvas.draw_rays(rt.propagate_rays(components, rays, 81 | lmb=lmb[idx]), colors[idx], 82 | linewidth=0.2) 83 | 84 | # Show the system 85 | canvas.show() 86 | 87 | # Save a copy 88 | canvas.save('grating.png') 89 | -------------------------------------------------------------------------------- /examples/field_lens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | import ray_utilities 12 | 13 | if __name__ == '__main__': 14 | # System to test a field lens. 15 | # Set f_field to 4e1 to get maximum throughput, and 4e9 (or something large) 16 | # to disable its effects 17 | 18 | # Constants 19 | ffl = 45 # Flange focal length 20 | f_field = 4e1 # Focal length of field lens 21 | aperture = 100 # Aperture of each lens. 22 | nrays = 20 # Number of rays per scene point 23 | npoints = 5 # Number of scene points 24 | image_plane = -300 # Position of image plane 25 | ymax = 200 # Limit of image plane 26 | ymin = -200 27 | 28 | # Create a scene. Hehe 29 | scene = np.zeros((2, npoints)) 30 | scene[0, :] = image_plane 31 | scene[1, :] = np.linspace(ymin, ymax, npoints) 32 | 33 | # Create an objective lens 34 | components = [] 35 | components.append(rt.Lens(f=-image_plane, 36 | aperture=aperture, 37 | pos=[-20,0], 38 | theta=0)) 39 | components.append(rt.Lens(f=ffl, 40 | aperture=aperture, 41 | pos=[0,0], 42 | theta=0)) 43 | 44 | # Add a field lens. 45 | components.append(rt.Lens(f=f_field, 46 | aperture=aperture, 47 | pos=[ffl,0], 48 | theta=0)) 49 | 50 | # Add a relay pair 51 | components.append(rt.Lens(f=50, 52 | aperture=aperture, 53 | pos=[ffl+50, 0], 54 | theta=0)) 55 | components.append(rt.Lens(f=50, 56 | aperture=aperture, 57 | pos=[ffl+50+10, 0], 58 | theta=0)) 59 | 60 | # Add an image sensor 61 | components.append(rt.Sensor(aperture=aperture, 62 | pos=[ffl+110, 0], 63 | theta=0)) 64 | 65 | # Get initial rays 66 | [rays, ptdict, colors] = ray_utilities.initial_rays(scene, 67 | components[0], 68 | nrays) 69 | 70 | # Propagate rays without sensor to compute vignetting 71 | ray_bundles = rt.propagate_rays(components[:-1], rays) 72 | vignetting = ray_utilities.vignetting(ray_bundles, ptdict) 73 | print('Vignetting: {}'.format(vignetting)) 74 | 75 | # Propagate the rays to draw 76 | ray_bundles = rt.propagate_rays(components, rays) 77 | 78 | # Create a new canvas 79 | canvas = vis.Canvas([image_plane, ffl+130], [ymin, ymax]) 80 | 81 | # Draw the rays 82 | colors = vis.get_colors(npoints, nrays, flatten=True) 83 | canvas.draw_rays(ray_bundles, colors) 84 | 85 | # Draw the components 86 | canvas.draw_components(components) 87 | 88 | # Show the canvas 89 | canvas.show() 90 | 91 | # Save the canvas 92 | canvas.save('field_lens.png') 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OptSys -- Optical systems simulator 2 | 3 | # About 4 | Simulate ray-tracing of optical system with first order approximation. This will 5 | be a handy tool to get an intial estimate for various components for your 6 | optical system. 7 | 8 | # Components 9 | The following components can be used: 10 | 11 | * **Lens**: Convex/concave lens with finite aperture. Uses lens maker's formula for 12 | computing output ray 13 | 14 | * **SphericalMirror**: Concave/convex mirror with finite aperture. Uses lens maker's formula for computing output ray 15 | 16 | * **Mirror**: Flat mirror with finite aperture. 17 | 18 | * **Grating**: Diffraction grating with set number of groves and finite aperture. 19 | As of now, only fixed order is supported. 20 | 21 | * **DMD**: Digital Micromirror device with zero pitch size and finite aperture. 22 | 23 | * **Sensor**: Image sensor which halts all ray propagation. Use it as image plane. 24 | 25 | # Extra functions 26 | Apart from placement and viewing of optical elements and rays, you can also: 27 | 28 | 1. Compute the light throughput of the system 29 | 30 | 2. Compute vingetting for the scene for a given system 31 | 32 | 3. Simulate system for different wavelengths 33 | 34 | # Usage 35 | Broadly, simulation consists of two parts 36 | 37 | * **Components**: A (python) list of various optical components with component 38 | specific paramenter, position, aperture and angle w.r.t y-axis. An example: 39 | ```python 40 | import raytracing as rt 41 | 42 | components = [] 43 | components.append(rt.Lens(f=100, 44 | aperture=25.4, 45 | pos=[0,0], 46 | theta=0)) 47 | ``` 48 | 49 | * **Rays**: A (python) list of 3-tuple of rays with x-coordinate, y-coordinate 50 | and angle w.r.t x-axis. An example: 51 | ```python 52 | import raytracing as rt 53 | 54 | rays = [] 55 | rays.append([-10, 0, -np.pi/6]) 56 | ``` 57 | 58 | Once you configure your system with components and rays, you can propagate the 59 | rays using the following command: 60 | ```python 61 | ray_bundles = propagate_rays(components, rays) 62 | 63 | ``` 64 | 65 | In order to view the output, you create a canvas and draw the components and 66 | rays. 67 | ```python 68 | import visualize as vis 69 | 70 | # Color for the rays 71 | colors = 'r' 72 | 73 | # Create a new canvas 74 | canvas = vis.Canvas([-200, 600], [-100, 100]) 75 | 76 | # Draw the components 77 | canvas.draw_components(components) 78 | 79 | # Draw the rays 80 | canvas.draw_rays(ray_bundles, colors) 81 | 82 | # Show the system 83 | canvas.show() 84 | 85 | # Save a copy 86 | canvas.save('example.png') 87 | ``` 88 | 89 | See examples folder for more information. 90 | 91 | # Other modules 92 | **lf_2d.py** : Includes several functions for simulation of 2D lightfields 93 | 94 | **lf_4d.py**: Includes several functions for manipulating 4D lightfields 95 | 96 | # TODO 97 | - [ ] Redo all examples with updated functions 98 | - [x] Implement convex/concave mirror 99 | - [ ] Implement thin prism 100 | - [ ] Implement image plane computation 101 | - [ ] Implement field lens optimization 102 | - [ ] Replace numpy commands with pytorch to enable SGD-based optimization 103 | 104 | **Authors**: 105 | * Vishwanath Saragadam (Postdoc, Rice University) 106 | * Aswin Sankaranarayanan (Professor, ECE, Carnegie Mellon University) 107 | -------------------------------------------------------------------------------- /modules/ray_utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | import raytracing as rt 7 | import visualize as vis 8 | 9 | def initial_rays(scene, objective, nrays=10): 10 | ''' 11 | Function to get an initial set of light rays for a given scene 12 | configuration and objective lens 13 | 14 | Inputs: 15 | scene: 2xN matrix of points 16 | objective: Lens instance for objective lens 17 | nrays: Number of rays per scene point 18 | 19 | Outputs: 20 | rays: nrays.N list of rays 21 | point_ray_dict: List with point to rays correspondence 22 | colors: nrays.N list of colors 23 | ''' 24 | rays = [] 25 | colors = [] 26 | point_ray_dict = [] 27 | 28 | N = scene.shape[1] 29 | x0 = objective.pos[0] 30 | y0 = objective.pos[1] 31 | r = objective.aperture 32 | theta = np.pi/2-objective.theta 33 | 34 | # Compute extent of objective lens 35 | x1 = x0 + 0.5*r*np.cos(theta) 36 | y1 = y0 + 0.5*r*np.sin(theta) 37 | 38 | x2 = x0 - 0.5*r*np.cos(theta) 39 | y2 = y0 - 0.5*r*np.sin(theta) 40 | 41 | # Now create rays for each scene point 42 | for idx in range(N): 43 | theta_min = np.arctan2(y1-scene[1, idx], x1-scene[0, idx]) 44 | theta_max = np.arctan2(y2-scene[1, idx], x2-scene[0, idx]) 45 | 46 | rays += ray_fan(scene[:, idx], [theta_min, theta_max], nrays) 47 | colors += [tuple(np.random.rand(4))]*nrays 48 | point_ray_dict.append(np.arange(idx*nrays, (idx+1)*nrays)) 49 | 50 | return rays, point_ray_dict, colors 51 | 52 | def ray_fan(pos, theta_lim, nrays): 53 | ''' 54 | Function to get a fan of rays from a point 55 | 56 | Inputs: 57 | orig: Origin of the point 58 | theta_lim: 2-tuple of angle limits in radians 59 | nrays: Number of rays to generate 60 | 61 | Output: 62 | rays: List of rays in 3-tuple format: x-coordinte, y-coordinate and 63 | angle 64 | ''' 65 | rays = [] 66 | angles = np.linspace(theta_lim[0], theta_lim[1], nrays) 67 | 68 | for angle in angles: 69 | rays.append([pos[0], pos[1], angle]) 70 | 71 | return rays 72 | 73 | def throughput(ray_bundles): 74 | ''' 75 | Compute throughput of a system based on number of rays passing through 76 | 77 | Inputs: 78 | ray_bundles: List of rays. See raytracing.propagate_rays() 79 | 80 | Output: 81 | thp: Total fraction of light energy that goes through the system 82 | ''' 83 | nrays = len(ray_bundles) 84 | propagated = [~np.isnan(ray_bundles[idx][-1, -1]) for idx in range(nrays)] 85 | 86 | return sum(propagated)/(1.0*nrays) 87 | 88 | def vignetting(ray_bundles, ptdict): 89 | ''' 90 | Estimate vignetting by computing point wise throughput 91 | 92 | Inputs: 93 | ray_bundles: List of rays. See raytracing.propagate_rays() 94 | ptdict: List of indices of rays for each point 95 | 96 | Outputs: 97 | vign: List of throughputs per point 98 | ''' 99 | # Compute throughput 100 | nrays = len(ray_bundles) 101 | propagated = [~np.isnan(ray_bundles[idx][-1, -1]) for idx in range(nrays)] 102 | propagated = np.array(propagated) 103 | 104 | vign = np.zeros(len(ptdict)) 105 | 106 | for idx in range(len(ptdict)): 107 | pt_indices = propagated[list(ptdict[idx])] 108 | vign[idx] = sum(pt_indices)/len(pt_indices) 109 | 110 | return vign 111 | -------------------------------------------------------------------------------- /examples/dmd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | sys.path.append('../modules') 5 | 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | 9 | import raytracing as rt 10 | import visualize as vis 11 | import ray_utilities 12 | 13 | if __name__ == '__main__': 14 | # System to test a DMD imager 15 | 16 | # Constants 17 | ffl = 45 # Flange focal length 18 | f_field = 40 # Focal length of field lens 19 | aperture = 25.4 # Aperture of each lens 20 | nrays = 20 # Number of rays per scene point 21 | npoints = 5 # Number of scene points 22 | image_plane = -300 # Position of image plane 23 | ymax = 50 # Limit of image plane 24 | ymin = -50 25 | 26 | # Create a scene parallel to DMD and completely in focus. 27 | scene = np.zeros((2, npoints)) 28 | scene[0, :] = image_plane 29 | scene[1, :] = np.linspace(ymin, ymax, npoints) 30 | 31 | # Create a simple objective lens 32 | components = [] 33 | components.append(rt.Lens(f=-image_plane, 34 | aperture=aperture, 35 | pos=[-20,0], 36 | theta=0)) 37 | components.append(rt.Lens(f=ffl, 38 | aperture=aperture, 39 | pos=[0,0], 40 | theta=0)) 41 | 42 | # Add a field lens to increase throughput 43 | components.append(rt.Lens(f=f_field, 44 | aperture=aperture, 45 | pos=[ffl,0], 46 | theta=0)) 47 | 48 | # Reimage using a relay pair 49 | components.append(rt.Lens(f=100, 50 | aperture=aperture, 51 | pos=[ffl+100, 0], 52 | theta=0)) 53 | components.append(rt.Lens(f=100, 54 | aperture=aperture, 55 | pos=[ffl+110, 0], 56 | theta=0)) 57 | 58 | # Add a field lens on DMD to increase throughput 59 | components.append(rt.Lens(f=100, 60 | aperture=aperture, 61 | pos=[ffl+210, 0], 62 | theta=0)) 63 | # And then a DMD 64 | components.append(rt.DMD(deflection=-12*np.pi/180, 65 | aperture=aperture, 66 | pos=[ffl+210, 0], 67 | theta=0)) 68 | 69 | # The field lens needs repetition 70 | components.append(rt.Lens(f=100, 71 | aperture=aperture, 72 | pos=[ffl+210, 0], 73 | theta=-np.pi)) 74 | 75 | # Refocusing lens 76 | components.append(rt.Lens(f=30, 77 | aperture=aperture, 78 | pos=[200, -25], 79 | theta=-204*np.pi/180)) 80 | 81 | # Get initial rays 82 | [rays, ptdict, colors] = ray_utilities.initial_rays(scene, 83 | components[0], 84 | nrays) 85 | 86 | # Propagate the rays 87 | ray_bundles = rt.propagate_rays(components, rays) 88 | 89 | # Create a new canvas 90 | canvas = vis.Canvas([image_plane, 300], [-100, ymax], figsize=[12, 6]) 91 | 92 | # Create unique colors for each point 93 | colors = vis.get_colors(npoints, nrays) 94 | colors_list = [] 95 | 96 | for color in colors: 97 | colors_list += color 98 | 99 | # Draw the rays 100 | canvas.draw_rays(ray_bundles, colors_list, linewidth=0.2) 101 | 102 | # Draw the components 103 | canvas.draw_components(components) 104 | 105 | # Show the canvas 106 | canvas.show() 107 | 108 | # Save the canvas 109 | canvas.save('dmd.png') 110 | -------------------------------------------------------------------------------- /modules/lf_4d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | I am trying to test operations on lightfield images. 5 | ''' 6 | 7 | import os 8 | import sys 9 | import time 10 | 11 | import numpy as np 12 | import scipy.linalg as lin 13 | import scipy.ndimage as ndim 14 | 15 | import matplotlib.pyplot as plt 16 | import matplotlib.image as mpimg 17 | import Image 18 | 19 | def LF_shape_check(LF): 20 | ''' 21 | Cute function to check if the light-field is a grayscale one or RGB. 22 | 23 | Inputs: 24 | LF: Light-field object, 4D if grayscale and 5D if RGB 25 | 26 | Outputs: 27 | lf_shape: First four dimensions of the lightfield 28 | isrgb: If LF is an RGB object, then return True 29 | ''' 30 | lf_shape = LF.shape 31 | if len(lf_shape) == 4: 32 | return lf_shape, False 33 | else: 34 | return lf_shape[:4], True 35 | 36 | def get_lenslet(LF): 37 | ''' 38 | Create a lenslet image from the Light field object. 39 | ''' 40 | [lf_shape, isrgb] = LF_shape_check(LF) 41 | [U, V, H, W] = lf_shape 42 | 43 | if isrgb: 44 | lenslet_image = np.zeros((U*H, V*W, 3)) 45 | else: 46 | lenslet_image = np.zeros((U*H, V*W)) 47 | 48 | for idx1 in range(U): 49 | for idx2 in range(V): 50 | if isrgb: 51 | lenslet_image[idx1:U*H:U, idx2:V*W:V, :] =\ 52 | LF[idx1, idx2, :, :, :] 53 | else: 54 | lenslet_image[idx1:U*H:U, idx2:V*W:V] = LF[idx1, idx2, :, :] 55 | 56 | return lenslet_image 57 | 58 | def af_slice(AF, x, y, zoom_factor=1.0, save=False, fname=None): 59 | ''' 60 | Display an AF image at given coordinates. 61 | 62 | Inputs: 63 | AF: Aperture-Focus stack. 64 | x: Horizontal coordinate of the image. 65 | y: Vertical coordinate of the iamge. 66 | zoom_factor: Factor to resize the image by. 67 | save: If True, save the image. 68 | fname: If save is True, save the image using this name. 69 | 70 | Outputs: 71 | None 72 | ''' 73 | [af_shape, isrgb] = LF_shape_check(AF) 74 | 75 | if isrgb: 76 | AF_img = ndim.zoom(AF[:, :, x, y, :], (zoom_factor, zoom_factor, 1)) 77 | else: 78 | AF_img = ndim.zoom(AF[:, :, x, y], zoom_factor) 79 | 80 | if save: 81 | Image.fromarray(AF_img.astype(np.uint8)).save(fname) 82 | 83 | Image.fromarray(AF_img.astype(np.uint8)).show() 84 | 85 | def LF2AF(LF): 86 | ''' 87 | Convert Light-field stack to Aperture-Focus stack. 88 | ''' 89 | [lf_shape, isrgb] = LF_shape_check(LF) 90 | [U, V, H, W] = lf_shape 91 | 92 | AF = np.zeros_like(LF) 93 | 94 | # Assume U = V for now. 95 | c = U//2 96 | aperture_array = np.linspace(0, c*np.sqrt(2), U+1)[1:] 97 | focus_array = np.linspace(-1, 1, U) 98 | 99 | for a_idx, aperture in enumerate(aperture_array): 100 | for f_idx, focus in enumerate(focus_array): 101 | AF[a_idx, f_idx] = get_af_image(LF, aperture, focus) 102 | 103 | return AF 104 | 105 | def get_af_image(LF, aperture=None, focus=0): 106 | ''' 107 | Get an image from light field with a specified aperture and focus 108 | settings. 109 | ''' 110 | [lf_shape, isrgb] = LF_shape_check(LF) 111 | [U, V, H, W] = lf_shape 112 | 113 | # Default aperture setting is full open. 114 | if aperture is None: 115 | aperture = np.sqrt(2)*(U//2) 116 | 117 | # Get aperture indices 118 | c = U//2 119 | x = np.arange(U, dtype=float) 120 | y = np.arange(V, dtype=float) 121 | 122 | [X, Y] = np.meshgrid(x, y) 123 | [aperture_x, aperture_y] = np.where(np.hypot(X-c, Y-c) <= aperture) 124 | 125 | # Now get Focused lightfield. 126 | Xoffset_array = np.linspace(-0.5, 0.5, U)*focus*U 127 | Yoffset_array = np.linspace(-0.5, 0.5, V)*focus*V 128 | 129 | LF_shift = np.zeros_like(LF) 130 | for idx1 in range(U): 131 | Xoffset = Xoffset_array[idx1] 132 | for idx2 in range(V): 133 | Yoffset = Yoffset_array[idx2] 134 | 135 | # Shift the image and reassign it to LF. 136 | if isrgb: 137 | LF_shift[idx1, idx2, :, :, :] = ndim.shift( 138 | LF[idx1, idx2, :, :, :], 139 | [Xoffset, Yoffset, 0]) 140 | else: 141 | LF_shift[idx1, idx2, :, :] = ndim.shift(LF[idx1, idx2, :, :], 142 | [Xoffset, Yoffset]) 143 | 144 | # Done. Now get the image. 145 | if isrgb: 146 | af_image = LF_shift[aperture_x, aperture_y, :, :, :].mean(0) 147 | else: 148 | af_image = LF_shift[aperture_x, aperture_y, :, :].mean(0) 149 | 150 | return af_image 151 | 152 | def aperture_change(LF): 153 | ''' 154 | Create images with changing aperture settings. 155 | ''' 156 | [lf_shape, isrgb] = LF_shape_check(LF) 157 | [U, V, H, W] = lf_shape 158 | 159 | # No sanity check done now. Assume U = v 160 | c = U//2 161 | 162 | radii_array = np.linspace(0, c*np.sqrt(2), U+1)[1:] 163 | 164 | # Create mesh grid of indices to sweep through radii and find the indices. 165 | x = np.arange(U, dtype=float) 166 | y = np.arange(V, dtype=float) 167 | 168 | [X, Y] = np.meshgrid(x, y) 169 | 170 | # Store the variable aperture images here. 171 | images = [] 172 | 173 | for idx, radius in enumerate(radii_array): 174 | [idx_x, idx_y] = np.where(np.hypot(X-c, Y-c) <= radius) 175 | if isrgb: 176 | im = LF[idx_x, idx_y, :, :, :].sum(0)/len(idx_x) 177 | else: 178 | im = LF[idx_x, idx_y, :, :].sum(0)/len(idx_x) 179 | images.append(im) 180 | 181 | return images 182 | 183 | def focus_change(LF): 184 | ''' 185 | Create images with changing focus settings. 186 | ''' 187 | U = LF.shape[0] # Assuming aperture dimensions are same in both 188 | # directions. 189 | focus_array = np.linspace(-1, 1, U, endpoint=True) 190 | images = [] 191 | 192 | for focus in focus_array: 193 | images.append(_focus(LF, focus)) 194 | 195 | return images 196 | 197 | def _focus(LF, focus_val): 198 | ''' 199 | Focus the light-field at a given focal value. 200 | ''' 201 | # I am writing this mostly from LFFiltShiftSum from LFToolbox0.4 202 | [lf_shape, isrgb] = LF_shape_check(LF) 203 | [U, V, H, W] = lf_shape 204 | 205 | # Image indices 206 | [X, Y] = np.meshgrid(range(H), range(W)) 207 | 208 | # Shifts in each direction. 209 | Xoffset_array = np.linspace(-0.5, 0.5, U)*focus_val*U 210 | Yoffset_array = np.linspace(-0.5, 0.5, V)*focus_val*V 211 | 212 | LF_shift = np.zeros_like(LF) 213 | for idx1 in range(U): 214 | Xoffset = Xoffset_array[idx1] 215 | for idx2 in range(V): 216 | Yoffset = Yoffset_array[idx2] 217 | 218 | # Shift the image and reassign it to LF. 219 | if isrgb: 220 | LF_shift[idx1, idx2, :, :, :] = ndim.shift( 221 | LF[idx1, idx2, :, :, :], 222 | [Xoffset, Yoffset, 0]) 223 | else: 224 | LF_shift[idx1, idx2, :, :] = ndim.shift(LF[idx1, idx2, :, :], 225 | [Xoffset, Yoffset]) 226 | 227 | # Now add up the light-field slices. 228 | return LF_shift.mean(0).mean(0) 229 | 230 | if __name__ == '__main__': 231 | # Load the Aperture focus stack 232 | AF = np.load('results/lego_af.npy') 233 | -------------------------------------------------------------------------------- /modules/visualize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import numpy as np 4 | 5 | import matplotlib.pyplot as plt 6 | from matplotlib import patches 7 | from matplotlib import lines 8 | from matplotlib import cm 9 | 10 | import raytracing as rt 11 | 12 | class Canvas(object): 13 | ''' 14 | Class definition for canvas to draw components and rays 15 | ''' 16 | def __init__(self, xlim, ylim, bbox=None, figsize=None, dpi=100): 17 | ''' 18 | Function to initialize a blank canvas. 19 | 20 | Inputs: 21 | xlim: Tuple with limits for x-axis 22 | ylim: Tuple with limits for y-axis 23 | bbox: Parameters for bounding box. If None, it is automatically 24 | assigned. 25 | figsize: 2-tuple of figure size in inches. If None, figure size 26 | is set to 1ftx1ft 27 | dpi: DPI resolution (default is 100) 28 | 29 | Outputs: 30 | None 31 | ''' 32 | # Create an empty matplotlib tool 33 | if figsize is not None: 34 | [self._canvas, self.axes] = plt.subplots(figsize=figsize) 35 | else: 36 | [self._canvas, self.axes] = plt.subplots() 37 | 38 | # Set x-coordinates and enable grid 39 | self.xlim = xlim 40 | self.ylim = ylim 41 | 42 | self.axes.axis('scaled') 43 | self.axes.set_xlim(xlim) 44 | self.axes.set_ylim(ylim) 45 | self.axes.grid(True) 46 | 47 | if bbox is None: 48 | bbox = bbox={'facecolor':'yellow', 'alpha':0.5} 49 | 50 | self.bbox = bbox 51 | 52 | def draw_components(self, components): 53 | ''' 54 | Function to draw optical components. 55 | 56 | Inputs: 57 | components: List of optical components 58 | 59 | Outputs: 60 | None 61 | ''' 62 | for component in components: 63 | # Precomputation 64 | xy = component.Hinv.dot(np.array([0, -component.aperture/2, 1])) 65 | if component.type == 'sensor': 66 | # Draw a rectangle with pattern 67 | dmd_img = patches.Rectangle(xy=xy, 68 | width=component.aperture*0.1, 69 | height=component.aperture, 70 | angle=-component.theta*180/np.pi, 71 | linestyle='dashed', 72 | hatch='+', 73 | color='c') 74 | self.axes.add_artist(dmd_img) 75 | dmd_img.set_alpha(1) 76 | elif component.type == 'lens': 77 | # Draw an elongated ellipse 78 | lens_img = patches.Ellipse(xy=component.pos, 79 | width=component.aperture*0.1, 80 | height=component.aperture, 81 | angle=-component.theta*180/np.pi) 82 | self.axes.add_artist(lens_img) 83 | lens_img.set_alpha(0.5) 84 | elif component.type == 'spherical_mirror': 85 | # Draw a rectangle 86 | mirror_img = patches.Rectangle(xy=xy[:2], 87 | width=component.aperture*0.05, 88 | height=component.aperture, 89 | angle=-component.theta*180/np.pi, 90 | color='g') 91 | self.axes.add_artist(mirror_img) 92 | mirror_img.set_alpha(1) 93 | elif component.type == 'mirror': 94 | # Draw a rectangle 95 | mirror_img = patches.Rectangle(xy=xy[:2], 96 | width=component.aperture*0.05, 97 | height=component.aperture, 98 | angle=-component.theta*180/np.pi, 99 | color='k') 100 | self.axes.add_artist(mirror_img) 101 | mirror_img.set_alpha(1) 102 | elif component.type == 'grating': 103 | # Draw a hatched rectangle 104 | grating_img = patches.Rectangle(xy=xy[:2], 105 | width=component.aperture*0.05, 106 | height=component.aperture, 107 | angle=-component.theta*180/np.pi, 108 | hatch='/', 109 | color='m') 110 | self.axes.add_artist(grating_img) 111 | grating_img.set_alpha(0.2) 112 | elif component.type == 'dmd': 113 | # Draw a sawtooth rectangle 114 | dmd_img = patches.Rectangle(xy=xy, 115 | width=component.aperture*0.1, 116 | height=component.aperture, 117 | angle=-component.theta*180/np.pi, 118 | linestyle='dashed', 119 | hatch='x', 120 | color='g') 121 | self.axes.add_artist(dmd_img) 122 | dmd_img.set_alpha(1) 123 | elif component.type == 'aperture': 124 | # Small rectangle 125 | aperture_img = patches.Rectangle(xy=xy, 126 | width=component.aperture*0.02, 127 | height=component.aperture, 128 | angle=-component.theta*180/np.pi, 129 | color='b') 130 | self.axes.add_artist(aperture_img) 131 | aperture_img.set_alpha(0.5) 132 | else: 133 | raise ValueError("Invalid component name") 134 | 135 | # Post addition 136 | if component.name is not None: 137 | xy = component.Hinv.dot(np.array([8, -component.aperture/2-8, 1])) 138 | self.axes.text(xy[0], 139 | xy[1], 140 | component.name, 141 | bbox=self.bbox) 142 | 143 | def draw_rays(self, ray_bundles, colors=None, linewidth=0.5, 144 | membership=None): 145 | ''' 146 | Function to draw rays propagating through the system 147 | 148 | Inputs: 149 | ray_bundles: List of rays for the components 150 | colors: Color for each ray. If None, colors are randomly 151 | generated 152 | linewidth: Width of each ray 153 | membership: Indices array that indicate what all rays belong 154 | to a single spatial point. 155 | ''' 156 | if colors is None: 157 | colors = [np.random.rand(3,1) for i in range(len(ray_bundles))] 158 | 159 | if membership is None: 160 | membership = [0 for i in range(len(ray_bundles))] 161 | 162 | # Make sure number of rays and number of colors are same 163 | if len(ray_bundles) != len(colors): 164 | raise ValueError("Need same number of colors as rays") 165 | 166 | if len(ray_bundles) != len(colors): 167 | raise ValueError("Need same number of members as rays") 168 | 169 | # Create line styles 170 | linestyles = ['-', '--', ':'] 171 | 172 | for r_idx, ray_bundle in enumerate(ray_bundles): 173 | # First N-1 points are easy to cover 174 | for idx in range(ray_bundle.shape[1]-1): 175 | if ray_bundle[0, idx] == float('nan'): 176 | break 177 | 178 | xmin = ray_bundle[0, idx] 179 | xmax = ray_bundle[0, idx+1] 180 | 181 | ymin = ray_bundle[1, idx] 182 | ymax = ray_bundle[1, idx+1] 183 | 184 | linestyle = linestyles[membership[r_idx]%3] 185 | line = lines.Line2D([xmin, xmax], 186 | [ymin, ymax], 187 | color=colors[r_idx], 188 | linewidth=linewidth, 189 | linestyle=linestyle) 190 | self.axes.add_line(line) 191 | 192 | # The last point has slope and starting point, so extend it till 193 | # end of canvas 194 | xmin = ray_bundle[0, -1] 195 | ymin = ray_bundle[1, -1] 196 | 197 | if xmin == float('nan'): 198 | return 199 | 200 | # Brute force by extending line by maximum distance 201 | dist = np.hypot(self.xlim[1]-self.xlim[0], 202 | self.ylim[1]-self.ylim[0]) 203 | xmax = xmin + dist*np.cos(ray_bundle[2, idx+1]) 204 | ymax = ymin + dist*np.sin(ray_bundle[2, idx+1]) 205 | 206 | line = lines.Line2D([xmin, xmax], 207 | [ymin, ymax], 208 | color=colors[r_idx], 209 | linewidth=linewidth, 210 | linestyle=linestyles[membership[r_idx]%3]) 211 | self.axes.add_line(line) 212 | 213 | def show(self): 214 | ''' 215 | Function to show the canvas 216 | ''' 217 | self._canvas.show() 218 | 219 | def save(self, savename): 220 | ''' 221 | Function to save the canvas 222 | ''' 223 | self._canvas.savefig(savename, 224 | bbox_inches='tight', 225 | dpi=600) 226 | 227 | def get_colors(nwvl, nrays, cmap='jet', flatten=False): 228 | ''' 229 | Get a list of colors for visualization. 230 | 231 | Inputs: 232 | nwvl: Number of wavelengths (or type of rays) 233 | nrays: Number of rays per wavelength (or for each type of rays) 234 | cmap: Colormap to use. Default is Jet 235 | flatten: If True, return a single list, else return a list of lists 236 | 237 | Outputs: 238 | colors_list: (Python) list with 3-tuple of colors 239 | ''' 240 | colors = cm.get_cmap(cmap)(np.linspace(0, 1, nwvl)) 241 | 242 | colors_list = [] 243 | 244 | for idx in range(nwvl): 245 | if flatten: 246 | colors_list += [colors[idx, :] for idx2 in range(nrays)] 247 | else: 248 | colors_list.append([colors[idx, :3] for idx2 in range(nrays)]) 249 | 250 | return colors_list 251 | -------------------------------------------------------------------------------- /modules/lf_2d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Module for rendering simple optical outputs using raytracing. 5 | ''' 6 | 7 | # System imports 8 | import os 9 | import sys 10 | import pdb 11 | 12 | # Scipy and related imports 13 | import numpy as np 14 | import scipy.linalg as lin 15 | import scipy.ndimage as ndim 16 | import matplotlib.pyplot as plt 17 | import Image 18 | 19 | # ------------------- Aperture Focus Image routines ---------------------------# 20 | def render_point(f, beta, d, screen_lim, res, h, x, gaussian_blur=True, 21 | gamma=0.7): 22 | ''' 23 | Render the image of a single point in a 2D setting with a thin lens 24 | model. 25 | 26 | Inputs: 27 | f: Focal length of the lens. 28 | beta: Aperture radius from optical axis. 29 | d: Distance of the screen from the lens. 30 | screen_lim: Limits of the screen along vertical axis. 31 | res: Resolution of the screen in terms of distance between two 32 | consecutive points. 33 | h: Height of the point from optical axis. 34 | x: Distance of the point from the lens. 35 | gaussian_blur: If True, use a gaussian blur for out of focus point, 36 | else use a pillbox blur model. 37 | gamma: Factor for gamma correction of the image. 38 | 39 | Outputs: 40 | screen: Array representing screen coordinates. 41 | image: Projection of the point on the screen. 42 | h0: Height of the image formed at the focal point. 43 | d0: Distance of the image formed at the focal point. 44 | ''' 45 | # Numerical stability 46 | f += 1e-15 47 | 48 | # Compute the point at which the image will be formed using thin lens 49 | # model. 50 | d0 = f*x/(x-f) 51 | 52 | # compute the height at which the image will actually be formed. 53 | h0 = -f*h/(x-f) 54 | 55 | # compute the position of the image on the sensor 56 | hs = -h*d/x 57 | 58 | # Compute the extent of blur. 59 | h1 = -d*(-h0+beta)/d0 + beta 60 | h2 = -d*(-h0-beta)/d0 - beta 61 | dh = abs(h2-h1) + 1e-10 62 | 63 | [llim, ulim] = screen_lim 64 | 65 | # Construct the screen and image. 66 | screen = np.arange(llim, ulim, res) 67 | image = np.zeros_like(screen) 68 | 69 | # Assume unit intensity for now. 70 | if gaussian_blur: 71 | image = np.exp(-pow(screen-hs, 2)/pow(dh, 2))/np.sqrt(2*np.pi)/dh 72 | image -= image[abs(screen-h1).argmin()] 73 | else: 74 | image[:] = 1 75 | 76 | # Clip and normalize. See if screen is in front or behind focus point. 77 | 78 | if d > d0: 79 | image *= (screen >= h1)*(screen <= h2) 80 | else: 81 | image *= (screen >= h2)*(screen <= h1) 82 | 83 | imsum = image.sum() 84 | 85 | # If imsum is zero, it means that the image is focussed. 86 | if imsum == 0: 87 | image[abs(screen-hs).argmin()] = 1/np.sqrt(2*np.pi)/dh 88 | 89 | # Do a gamma correction. 90 | image = pow(abs(image), gamma) 91 | 92 | # Done. We can return the image. 93 | return screen, image, h0, d0 94 | 95 | def afi_point(f_array, beta_array, d, screen_lim, res, h, x, 96 | gaussian_blur=True): 97 | ''' 98 | Render an aperture focus stack for a single point using a thin lens 99 | model. 100 | 101 | Inputs: 102 | f_array: Focal length of the lens. 103 | beta_array: Array of aperture values. 104 | d: Distance between lens and sensor. 105 | screen_lim: Coordinate limits of sensor. 106 | res: Resolution of the sensor. 107 | h: Height of the point from optical axis. 108 | x: Distance of the point from the lens. 109 | gaussian_blur: If True, use the gaussian blur model, else use the 110 | pillbox blur model. 111 | 112 | Outputs: 113 | screen: Array representing screen coordinates. 114 | AF: Aperture focus stack. 115 | h0_array: Array of original focus point of the object. 116 | d0_array: Array of distances from lens of focus point. 117 | ''' 118 | pdb.set_trace() 119 | # Compute screen for one configuration. 120 | [screen, img, h0, d0] = render_point(f_array[0], beta_array[0], d, 121 | screen_lim, res, h, x, 122 | gaussian_blur) 123 | 124 | # Create the AF object. 125 | AF = np.zeros((len(f_array), len(beta_array), len(screen))) 126 | h0_array = np.zeros(len(f_array)) 127 | d0_array = np.zeros(len(f_array)) 128 | 129 | for f_idx, f in enumerate(f_array): 130 | for b_idx, beta in enumerate(beta_array): 131 | [_, im, h0, d0] = render_point(f, beta, d, screen_lim, res, h, x, 132 | gaussian_blur) 133 | AF[f_idx, b_idx] = im 134 | h0_array[f_idx] = h0 135 | d0_array[f_idx] = d0 136 | 137 | # Compute the screen index where the image is focussed. 138 | focus_idx = abs(d0_array - d).argmin() 139 | im_idx = abs(screen - h0_array[focus_idx]).argmin() 140 | 141 | # Done. 142 | return screen, AF, im_idx 143 | 144 | def afi_render(f_array, beta_array, d, screen_lim, res, scene, 145 | gaussian_blur=True): 146 | ''' 147 | Render an aperture focus stack for a simple scene using a thin lens 148 | model. 149 | 150 | Inputs: 151 | f_array: Focal length of the lens. 152 | beta_array: Array of aperture values. 153 | d: Distance between lens and sensor. 154 | screen_lim: Coordinate limits of sensor. 155 | res: Resolution of the sensor. 156 | scene: A 3-tuple representing the scene geometry: 157 | scene[1]: Heights of points from optical axis 158 | scene[2]: Distance of poitns from the lens 159 | scene[3]: Intensities of the points (0-1). 160 | gaussian_blur: If True, use the gaussian blur model, else use the 161 | pillbox blur model. 162 | 163 | Outputs: 164 | screen: Array representing screen coordinates. 165 | AF: Aperture focus stack. 166 | im_idx_array: Array of screen coordinates where each scene point 167 | is best focused. 168 | ''' 169 | # Unpack the scene 170 | [H, X, I] = scene 171 | 172 | # Compute an AFI for dimensions. 173 | [screen, AF, im_idx] = afi_point(f_array, beta_array, d, screen_lim, 174 | res, H[0], X[0], gaussian_blur) 175 | im_idx_array = np.zeros(len(H)) 176 | 177 | # Use the above AFI and im_idx 178 | AF *= I[0] 179 | im_idx_array[0] = im_idx 180 | 181 | # For now, the AFIs are just added up. No obstacle based ray tracing done. 182 | for idx in range(1, len(H)): 183 | [_, AF_tmp, im_idx] = afi_point(f_array, beta_array, d, 184 | res, H[idx], X[idx], 185 | gaussian_blur) 186 | AF += AF_time*I[idx] 187 | im_idx_array[idx] = im_idx 188 | 189 | return screen, AF, im_idx_array 190 | 191 | def plot_af(AF, screen): 192 | ''' 193 | Plot the superimposed images of AF stack. 194 | 195 | Inputs: 196 | AF: Aperture focus stack. 197 | screen: Coordinates of the sensor screen. 198 | 199 | Outputs: 200 | None 201 | ''' 202 | [nf, na, ns] = AF.shape 203 | 204 | for f_idx in range(nf): 205 | for a_idx in range(na): 206 | plt.plot(screen, AF[f_idx, a_idx, :]) 207 | 208 | plt.show() 209 | 210 | def show_af_slice(AF, im_idx, zoom_factor=1.0, save=False, fname=None, 211 | show=True): 212 | ''' 213 | Show the AF image slice for a 2D image setting. 214 | 215 | Inputs: 216 | AF: The aperture focus stack 217 | im_idx: Index to extract the AF slice from. 218 | zoom_factor: Amount to zoom the image by. 219 | save: If True, save the image. 220 | fname: If save is true, save the image using this name. 221 | show: If False, just save and don't show. 222 | 223 | Output: 224 | None. 225 | ''' 226 | AF_im = abs(AF[:, :, im_idx]) 227 | AF_im = ndim.zoom(AF_im.T*255/AF_im.max(), zoom_factor) 228 | if show: 229 | Image.fromarray(AF_im).show() 230 | 231 | if save: 232 | Image.fromarray(AF_im.astype(np.uint8)).save(fname) 233 | 234 | # --------------------- Light field routines ----------------------------------# 235 | def lf_point(f, d, lens_lim, screen_lim, lens_res, screen_res, h, x, 236 | intensity=1): 237 | ''' 238 | Render the light field output of a single point. 239 | 240 | Inputs: 241 | f: Focal length of the lens. 242 | d: Distance of the screen from the lens. 243 | lens_lim: Limits of the lens plane(u). 244 | screen_lim: Limits of the screen plane(s). 245 | lens_res: Resolution of sampling of the lens plane. 246 | screen_res: REsolution of sampling of the screen plane. 247 | h: Distance of point from the lens. 248 | x: Height of point from the optical axis. 249 | intensity: Radiance emitted by the point. Default is 1. 250 | 251 | Outputs: 252 | lens: Coordinates of sampling on the lens. 253 | screen: Coordinates of sampling on the screen. 254 | h0: Focal point of the image. 255 | LF: Light field image, LF(u, s) 256 | ''' 257 | # Compute the point at which the image will be formed using thin lens 258 | # model. 259 | d0 = f*x/(x-f) 260 | 261 | # compute the height at which the image will actually be formed. 262 | h0 = -f*h/(x-f) 263 | 264 | # compute the position of the image on the sensor 265 | hs = -h*d/x 266 | 267 | [llim_l, ulim_l] = lens_lim 268 | 269 | # Compute the extent of light-field. 270 | h1 = -d*(-h0+ulim_l)/d0 + ulim_l 271 | h2 = -d*(-h0-llim_l)/d0 - llim_l 272 | dh = abs(h2-h1) + 1e-10 273 | 274 | [llim_s, ulim_s] = screen_lim 275 | 276 | # Construct the screen, lens and light field. 277 | screen = np.arange(llim_s, ulim_s, screen_res) 278 | lens = np.arange(llim_l, ulim_l, lens_res) 279 | 280 | LF = np.zeros((len(screen), len(lens))) 281 | 282 | # Compute the light field coordinates. 283 | S = lens - (d/d0)*(lens-h0) 284 | 285 | for idx, u in enumerate(lens): 286 | s = S[idx] 287 | LF[abs(screen-s).argmin(), idx] = intensity 288 | 289 | return lens, screen, h0, LF 290 | 291 | def lf_render(f, d, lens_lim, screen_lim, lens_res, screen_res, scene): 292 | ''' 293 | Render the light field output of a single point. 294 | 295 | Inputs: 296 | f: Focal length of the lens. 297 | d: Distance of the screen from the lens. 298 | lens_lim: Limits of the lens plane(u). 299 | screen_lim: Limits of the screen plane(s). 300 | lens_res: Resolution of sampling of the lens plane. 301 | screen_res: Resolution of sampling of the screen plane. 302 | scene: A 3-tuple representing the scene geometry: 303 | scene[1]: Heights of points from optical axis 304 | scene[2]: Distance of poitns from the lens 305 | scene[3]: Intensities of the points(0-1). 306 | 307 | Outputs: 308 | lens: Coordinates of sampling on the lens. 309 | screen: Coordinates of sampling on the screen. 310 | h0: Focal point of the image. 311 | LF: Light field image, LF(u, s) 312 | ''' 313 | # Extract the scene: 314 | [H, D, I] = scene 315 | 316 | # Sort the scene by distances from lens. A point nearer to the lens will 317 | # obstruct anything behind, hence the sorting. 318 | sort_idx = np.argsort(D)[::-1] 319 | 320 | # Render an example first point LF. 321 | idx = sort_idx[0] 322 | [lens, screen, h0, LF] = lf_point(f, d, lens_lim, screen_lim, lens_res, 323 | screen_res, H[idx], D[idx], I[idx]) 324 | # We want to save the focus heights as well. 325 | h0_array = np.zeros_like(H) 326 | 327 | for idx in sort_idx[1:]: 328 | # Compute a new LF for each point. 329 | [_, _, h0, lf] = lf_point(f, d, lens_lim, screen_lim, lens_res, 330 | screen_res, H[idx], D[idx], I[idx]) 331 | # The new computed light field covers the previous light field. 332 | LF[lf != 0] = lf[lf != 0] 333 | h0_array[idx] = h0 334 | 335 | # Done. 336 | return lens, screen, h0_array, LF 337 | -------------------------------------------------------------------------------- /modules/raytracing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Module for rendering simple optical outputs using raytracing. 5 | ''' 6 | 7 | # System imports 8 | import os 9 | import sys 10 | import pdb 11 | 12 | # Scipy and related imports 13 | import numpy as np 14 | import scipy.linalg as lin 15 | import scipy.ndimage as ndim 16 | import matplotlib.pyplot as plt 17 | 18 | def propagate_rays(components, rays, lmb=525e-9): 19 | ''' 20 | Function to propagate rays through a set of components 21 | 22 | Inputs: 23 | components: List of optical components 24 | rays: List of 3-tuple of rays with x-coord, y-coord and angle 25 | lmb: Wavelength of rays in m. Default if 525nm 26 | 27 | Outputs: 28 | ray_bundles: For N components, this is a list of 3x(N+1) coordinates 29 | of rays propagated through the components. 30 | ''' 31 | # Create output ray first 32 | ncomponents = len(components) 33 | nrays = len(rays) 34 | ray_bundles = [] 35 | 36 | for idx in range(nrays): 37 | ray_bundles.append(np.zeros((3, ncomponents+1))) 38 | ray_bundles[idx][:, 0] = rays[idx] 39 | 40 | # Now propagate each ray through each component 41 | for r_idx in range(nrays): 42 | for c_idx in range(1, ncomponents+1): 43 | ray_bundles[r_idx][:, c_idx] = components[c_idx-1].propagate( 44 | ray_bundles[r_idx][:, c_idx-1], 45 | lmb).reshape(3,) 46 | 47 | # Done, return 48 | return ray_bundles 49 | 50 | def angle_wrap(angle): 51 | ''' 52 | Wrap angle between -180 to 180 53 | 54 | Inputs: 55 | angle: In radians 56 | 57 | Outputs: 58 | wrapped_angle: In radians 59 | ''' 60 | if angle > np.pi: 61 | angle -= 2*np.pi 62 | elif angle < -np.pi: 63 | angle += 2*np.pi 64 | 65 | return angle 66 | 67 | class OpticalObject(object): 68 | ''' 69 | Generic class definition for optical object. This object is inherited 70 | to create optical objects such as lenses, mirrors and gratings 71 | 72 | Common properties: 73 | 1. Position of the object 74 | 2. Orientation of the object w.r.t global Y axis 75 | 3. Aperture: Diameter of the object 76 | 77 | Specific properties: 78 | Lens/mirror: Focal length 79 | Grating (Transmissive only): Number of groves per mm 80 | DMD: Deflection angle 81 | 82 | Note: Unless you definitely know what you are doing, do not create 83 | any object with this class. Look at the objects which inherit 84 | this class. 85 | ''' 86 | def __init__(self, aperture, pos, theta, name=None): 87 | ''' 88 | Generic constructor for optical objects. 89 | 90 | Inputs: 91 | aperture: Aperture size 92 | pos: Position of lens in 2D cartesian grid 93 | theta: Inclination of lens w.r.t Y axis 94 | name: Name (string) of the optical component. Name will be 95 | used for labelling the ocmponents in drawing 96 | 97 | Outputs: 98 | None. 99 | ''' 100 | self.theta = theta 101 | self.pos = pos 102 | self.aperture = aperture 103 | self.name = name 104 | 105 | # Create coordinate transformation matrix 106 | self.H = self.create_xform() 107 | self.Hinv = lin.inv(self.H) 108 | 109 | def get_intersection(self, orig, theta): 110 | ''' 111 | Method to get interesection of optical object plane and ray 112 | 113 | Inputs: 114 | orig: Origin of ray 115 | theta: Orientation of ray w.r.t x-axis 116 | 117 | Outputs: 118 | dest: Destination of ray 119 | ''' 120 | 121 | # If theta is nan, it means the ray terminated 122 | if np.isnan(theta): 123 | return np.ones((3,1))*float('nan') 124 | 125 | # Transform ray origin to new coordinates 126 | p = np.array([[orig[0]], [orig[1]], [1]], dtype=np.float64) 127 | 128 | p_new = self.H.dot(p) 129 | 130 | # Similarly find the angle in new coordinate system 131 | theta_new = theta + self.theta 132 | 133 | # Now compute the intersection 134 | x_new_tf = 0 135 | y_new_tf = p_new[1][0] - p_new[0][0]*np.tan(theta_new) 136 | 137 | # Sanity check to see if the interesection lies within the aperture 138 | if y_new_tf > self.aperture/2.0 or y_new_tf < -self.aperture/2.0: 139 | flag = float('nan') 140 | else: 141 | flag = 1.0 142 | 143 | p_tf = np.array([[x_new_tf], [y_new_tf], [1]]) 144 | 145 | # Go back to original system and return result 146 | p_final = self.Hinv.dot(p_tf) 147 | p_final[2] = flag 148 | 149 | return p_final 150 | 151 | def propagate(self, point, lmb=None): 152 | ''' 153 | Function to propagate a ray through the object. Requires an extra 154 | definition for computing angles 155 | 156 | Inputs: 157 | point: 3x1 vector with x-coordinate, y-coordinate and angle (rad) 158 | lmb: Wavelength. Only required for grating 159 | Outputs: 160 | dest: 3x1 vector with x-coordinate, y-coordinate and angle (rad) 161 | ''' 162 | # First get intersection of the point with object plane 163 | dest = self.get_intersection(point[:2], point[2]) 164 | 165 | # Then compute angle 166 | if np.isnan(dest[2]): 167 | return dest 168 | 169 | dest[2] = self._get_angle(point, lmb, dest) 170 | 171 | return dest 172 | 173 | def create_xform(self): 174 | ''' 175 | Function to create transformation matrix for coordinate change 176 | 177 | Inputs: 178 | None 179 | 180 | Outputs: 181 | H: 3D transformation matrix 182 | ''' 183 | R = np.zeros((3, 3)) 184 | T = np.zeros((3, 3)) 185 | 186 | # Rotation 187 | R[0, 0] = np.cos(self.theta) 188 | R[0, 1] = -np.sin(self.theta) 189 | R[1, 0] = np.sin(self.theta) 190 | R[1, 1] = np.cos(self.theta) 191 | R[2, 2] = 1 192 | 193 | # Translation 194 | T[0, 0] = 1 195 | T[0, 2] = -self.pos[0] 196 | T[1, 1] = 1 197 | T[1, 2] = -self.pos[1] 198 | T[2, 2] = 1 199 | 200 | return R.dot(T) 201 | class Sensor(OpticalObject): 202 | ''' Class definition for a sensor object''' 203 | def __init__(self, aperture, pos, theta, name='Sensor'): 204 | ''' 205 | Constructor for sensor object. Sensor is simply a plan which blocks 206 | all rays 207 | 208 | Inputs: 209 | aperture: Size of sensor 210 | pos: Position of sensor in 2D cartesian grid 211 | theta: Inclination of sensor w.r.t Y axis 212 | name: Name of the optical component. If Empty string, generic 213 | name is assigned. If None, no name is printed. 214 | 215 | Outputs: 216 | None. 217 | ''' 218 | # Initialize parent optical object parameters 219 | OpticalObject.__init__(self, aperture, pos, theta, name) 220 | 221 | # Extra parameters 222 | self.type = 'sensor' 223 | 224 | def _get_angle(self, point, lmb, dest): 225 | ''' 226 | Angle after propagation. Since this is a sensor, return NaN to flag 227 | end of ray 228 | 229 | Inputs: 230 | point: 3-tuple point with x, y, angle 231 | lmb: Wavelength of ray. Only needed for grating 232 | dest: 2D coordinate of interesection of ray with plane 233 | 234 | Outputs: 235 | theta: NaN, since this is a sensor 236 | ''' 237 | return float('NaN') 238 | 239 | class Lens(OpticalObject): 240 | ''' Class definition for lens object''' 241 | def __init__(self, f, aperture, pos, theta, name=""): 242 | ''' 243 | Constructor for lens object. 244 | 245 | Inputs: 246 | f: Focal length of lens 247 | aperture: Aperture size 248 | pos: Position of lens in 2D cartesian grid 249 | theta: Inclination of lens w.r.t Y axis 250 | name: Name of the optical component. If Empty string, generic 251 | name is assigned. If None, no name is printed. 252 | 253 | Outputs: 254 | None. 255 | ''' 256 | if name == "": 257 | name = 'f = %d'%f 258 | 259 | # Initialize parent optical object parameters 260 | OpticalObject.__init__(self, aperture, pos, theta, name) 261 | 262 | # Extra parameters 263 | self.f = f 264 | self.type = 'lens' 265 | 266 | def _get_angle(self, point, lmb, dest): 267 | ''' 268 | Angle after propagation. This function is used by propagate function 269 | of master class. Do not use it by itself. 270 | 271 | Inputs: 272 | point: 3-tuple point with x, y, angle 273 | lmb: Wavelenght of ray. Only needed for grating 274 | dest: 2D coordinate of interesection of ray with plane 275 | 276 | Outputs: 277 | theta: Angle after propagation. 278 | ''' 279 | # Find the point on focal plane where all parallel rays meet 280 | focal_dest = self.Hinv.dot(np.array([[self.f], 281 | [self.f*np.tan(point[2]+self.theta)], 282 | [1]])) 283 | # Now find the angle 284 | theta = np.arctan2(focal_dest[1]-dest[1],focal_dest[0]-dest[0]) 285 | 286 | # For concave lens, add 180 degrees 287 | if self.f < 0: 288 | theta += np.pi 289 | 290 | return theta 291 | 292 | class SphericalMirror(OpticalObject): 293 | ''' Class definition for spherical mirror object''' 294 | def __init__(self, f, aperture, pos, theta, name=""): 295 | ''' 296 | Constructor for spherical mirror object. 297 | 298 | Inputs: 299 | f: Focal length of mirror -- positive for concave and negative 300 | for convex 301 | aperture: Aperture size 302 | pos: Position of mirror in 2D cartesian grid 303 | theta: Inclination of mirror w.r.t Y axis 304 | name: Name of the optical component. If Empty string, generic 305 | name is assigned. If None, no name is printed. 306 | 307 | Outputs: 308 | None. 309 | ''' 310 | if name == "": 311 | name = 'f = %d'%f 312 | 313 | # Initialize parent optical object parameters 314 | OpticalObject.__init__(self, aperture, pos, theta, name) 315 | 316 | # Extra parameters 317 | self.f = f 318 | self.type = 'spherical_mirror' 319 | 320 | def _get_angle(self, point, lmb, dest): 321 | ''' 322 | Angle after propagation. This function is used by propagate function 323 | of master class. Do not use it by itself. 324 | 325 | Inputs: 326 | point: 3-tuple point with x, y, angle 327 | lmb: Wavelenght of ray. Only needed for grating 328 | dest: 2D coordinate of interesection of ray with plane 329 | 330 | Outputs: 331 | theta: Angle after propagation. 332 | ''' 333 | # Find the point on focal plane where all parallel rays meet 334 | focal_dest = self.Hinv.dot(np.array([[-self.f], 335 | [self.f*np.tan(point[2]+self.theta)], 336 | [1]])) 337 | # Now find the angle 338 | theta = np.arctan2(focal_dest[1]-dest[1], focal_dest[0]-dest[0]) 339 | 340 | # For convex lens, add 180 degrees 341 | if self.f < 0: 342 | theta += np.pi 343 | 344 | return theta 345 | 346 | class Grating(OpticalObject): 347 | ''' Class definition for a diffraction grating''' 348 | def __init__(self, ngroves, aperture, pos, theta, m=1, transmissive=True, 349 | name='Grating'): 350 | ''' 351 | Constructor for Grating object. 352 | 353 | Inputs: 354 | ngroves: Number of groves per mm. 355 | aperture: Size of diffraction grating 356 | pos: Position of diffraction grating 357 | theta: Inclination of theta w.r.t Y axis 358 | m: Order of diffraction. If you want light to diffract on the 359 | other side, use m=-1 360 | transmissive: If True, diffraction grating is treated as being 361 | transmissive, else is treated as reflective 362 | name: Name of the optical component. If Empty string, generic 363 | name is assigned. If None, no name is printed. 364 | 365 | Outputs: 366 | None. 367 | ''' 368 | 369 | # Initialize parent optical object parameters 370 | OpticalObject.__init__(self, aperture, pos, theta) 371 | 372 | # Extra parameters 373 | self.ngroves = ngroves 374 | self.m = m 375 | self.type = 'grating' 376 | 377 | def _get_angle(self, point, lmb, dest): 378 | ''' 379 | Function to compute angle after propagation. This is used by master 380 | class, do not use it by itself. 381 | 382 | Inputs: 383 | point: 3-tuple of x-coordinate, y-coordinate and angle (radian) 384 | lmb: Wavelength of the ray. 385 | dest: 2D coordinate of interesection of ray with plane 386 | 387 | Outputs: 388 | lmb: Angle after propagation 389 | ''' 390 | # Compute destination first 391 | dest = self.get_intersection(point[:2], point[2]) 392 | 393 | # Next find output angle 394 | incident_theta = point[2] + self.theta 395 | 396 | a = 1e-3/self.ngroves 397 | refracted_theta = np.arcsin(np.sin(incident_theta) - self.m*lmb/a) 398 | 399 | return angle_wrap(refracted_theta - self.theta) 400 | 401 | class Mirror(OpticalObject): 402 | ''' Class definition for Mirror object''' 403 | def __init__(self, aperture, pos, theta, name='Mirror'): 404 | ''' 405 | Constructor for Mirror object. 406 | 407 | Inputs: 408 | aperture: Size of diffraction grating 409 | pos: Position of diffraction grating 410 | theta: Inclination of theta w.r.t Y axis 411 | name: Name of the optical component. If Empty string, generic 412 | name is assigned. If None, no name is printed. 413 | 414 | Outputs: 415 | None. 416 | ''' 417 | 418 | # Initialize parent optical object parameters 419 | OpticalObject.__init__(self, aperture, pos, theta, name) 420 | 421 | # Extra parameters 422 | self.type = 'mirror' 423 | 424 | def _get_angle(self, point, lmb, dest): 425 | ''' 426 | Function to compute angle after propagation through mirror. 427 | 428 | Inputs: 429 | point: 3-tuple of x-coordinate, y-coordinate and angle (radians) 430 | lmb: Wavelength of ray, only needed for grating 431 | 432 | Outputs: 433 | theta: Angle after propagation 434 | ''' 435 | 436 | # Next compute deflection angle 437 | theta_dest = np.pi - point[2] - 2*self.theta 438 | 439 | return angle_wrap(theta_dest) 440 | 441 | class DMD(OpticalObject): 442 | ''' Class definition for DMD object''' 443 | def __init__(self, deflection, aperture, pos, theta, name='DMD'): 444 | ''' 445 | Constructor for DMD object. 446 | 447 | Inputs: 448 | deflection: Deflection angle of DMD 449 | aperture: Size of diffraction grating 450 | pos: Position of diffraction grating 451 | theta: Inclination of theta w.r.t Y axis 452 | name: Name of the optical component. If Empty string, generic 453 | name is assigned. If None, no name is printed. 454 | 455 | Outputs: 456 | None. 457 | ''' 458 | 459 | # Initialize parent optical object parameters 460 | OpticalObject.__init__(self, aperture, pos, theta, name) 461 | 462 | # Extra parameters 463 | self.deflection = deflection 464 | self.type = 'dmd' 465 | 466 | def _get_angle(self, point, lmb=None, dest=None): 467 | ''' 468 | Function to compute angle after propagation 469 | 470 | Inputs: 471 | point: 3-tuple of x-coordinate, y-coordinate, angle (radian) 472 | lmb: Wavelength of ray. Not relevant for DMD 473 | 474 | Outputs: 475 | theta: Angle after propagation 476 | ''' 477 | return angle_wrap(np.pi - point[2] - 2*(self.theta + self.deflection)) 478 | 479 | class Aperture(OpticalObject): 480 | ''' Class definition for aperture object''' 481 | def __init__(self, aperture, pos, theta, name='Aperture'): 482 | ''' 483 | Function to initialize the aperture object 484 | 485 | Inputs: 486 | aperture: Size of the aperture 487 | pos: Position of the aperture 488 | theta: Clockwise rotation of aperture w.r.t y-axis in radians 489 | name: Name of the Optical object. Default is 'Aperture' 490 | ''' 491 | 492 | # Initialize parent optical object parameters 493 | OpticalObject.__init__(self, aperture, pos, theta, name) 494 | 495 | # Extra parameters 496 | self.type = 'aperture' 497 | 498 | def _get_angle(self, point, lmb=None, dest=None): 499 | ''' 500 | Function to compute output angle after propagation 501 | 502 | Inputs: 503 | point: 3-tuple of x-coordinate, y-coordinate, angle (radians) 504 | lmb: Wavelength of ray. Not relevant. 505 | 506 | Outputs: 507 | theta: Angle after propagation 508 | ''' 509 | # Simply return the angle 510 | return point[2] 511 | --------------------------------------------------------------------------------