├── LICENSE ├── POsolver.py ├── README.md ├── Test_RCS.py ├── geometries ├── dihedral.obj ├── dihedral_offset.obj ├── plate.obj └── trihedral.obj └── test_range.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 pingpongballz 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 | -------------------------------------------------------------------------------- /POsolver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import igl 3 | from rtxpy import RTX, has_cupy 4 | import rtxpy 5 | import matplotlib.pyplot as plt 6 | 7 | 8 | 9 | optix = RTX() 10 | 11 | 12 | 13 | #helper fns 14 | cosd = lambda x : np.cos(np.deg2rad(x)) 15 | sind = lambda x : np.sin(np.deg2rad(x)) 16 | 17 | def ortho_set(phi, theta): 18 | right = [0,0,0]; up = [0,0,0]; normal = [0,0,0]; 19 | 20 | normal[0] = sind(theta)*cosd(phi) 21 | normal[1] = sind(theta)*sind(phi) 22 | normal[2] = cosd(theta) 23 | 24 | right[0] = sind(phi) 25 | right[1] = -cosd(phi) 26 | up = np.cross(right, normal) 27 | 28 | return np.array(normal), up, np.array(right) 29 | 30 | def polarise(pol, k_inc, k_ref, normal): 31 | e_perp = np.cross(k_inc, normal)/(np.linalg.norm(np.cross(k_inc, normal),axis = 1).reshape(-1,1)) 32 | 33 | e_par = np.cross(k_inc, e_perp)/(np.linalg.norm(np.cross(k_inc, e_perp),axis = 1).reshape(-1,1)) 34 | 35 | e_ref_perp = e_perp 36 | 37 | e_ref_par = np.cross(k_ref, e_ref_perp)/(np.linalg.norm(np.cross(k_ref, e_ref_perp),axis = 1).reshape(-1,1)) 38 | 39 | E_par = (pol*e_par).sum(1).reshape(-1,1) #I guessed this 40 | E_perp = (pol*e_perp).sum(1).reshape(-1,1) #I guessed this 41 | 42 | 43 | return e_ref_par*E_par - e_ref_perp*E_perp 44 | 45 | 46 | def build(filename): 47 | 48 | #Read file, extract vertices and faces, build env in OptiX 49 | v, f = igl.read_triangle_mesh(filename) 50 | 51 | verts = v.flatten() 52 | verts = np.float32(verts) 53 | triangles = f.flatten() 54 | triangles = np.int32(triangles) 55 | 56 | 57 | 58 | res = optix.build(0, verts, triangles) #PASS 2 GPU 59 | assert res == 0 60 | return v,f 61 | 62 | def shoot_and_record(hits_1, ray_pos, ray_dict, numrays): 63 | 64 | 65 | ray_temp = ray_pos[hits_1[:, 0] > 0, :] 66 | hit_temp = hits_1[hits_1[:, 0] > 0, :] 67 | o = ray_temp[:,0:3] 68 | k = ray_temp[:,4:7] 69 | n = hit_temp[:,1:4] 70 | p = (o + hit_temp[:,0:1]*k) + 1e-6 * n 71 | direction = k - 2* ((k*n).sum(1)).reshape(-1,1) *n 72 | ray_pos[hits_1[:, 0] > 0,0:3] = p 73 | ray_pos[hits_1[:, 0] > 0,4:7] = direction 74 | 75 | 76 | #update dictionary 77 | ray_dict[hits_1[:, 0] > 0,0] += hits_1[hits_1[:, 0] > 0,0] 78 | newpol = polarise(ray_dict[hits_1[:, 0] > 0, 1:4], k, direction, n) 79 | 80 | 81 | ray_dict[hits_1[:, 0] > 0, 1:4] = newpol 82 | 83 | numrays_1 = np.count_nonzero(hits_1[:, 0]>0) 84 | 85 | 86 | if numrays_1 > 0: 87 | hits = [[0,0,0,0] for i in range(numrays)] 88 | hits = np.float32(hits) 89 | hits = hits.flatten() 90 | 91 | rays = np.float32(ray_pos) 92 | rays = rays.flatten() 93 | 94 | res = optix.trace(rays, hits, numrays)#PASS 2 GPU 95 | assert res == 0 96 | 97 | hits_1 = hits.reshape(numrays,4) 98 | 99 | return hits_1, ray_pos, ray_dict 100 | 101 | def PO_Integral(ray_pos, r, pol, direction, tubediam, lam, dir_phi, dir_theta, dir_r, r0): 102 | 103 | k = 2*np.pi/lam 104 | rayArea = tubediam*tubediam 105 | r_vec = k * dir_r 106 | 107 | E_ap = pol * np.exp(-1j * k*r) 108 | 109 | H_ap = np.cross(direction, E_ap) 110 | 111 | B_theta = ((np.cross(-dir_phi, E_ap) + np.cross(dir_theta, H_ap))*direction).sum(1) 112 | B_phi = ((np.cross(dir_theta, E_ap) + np.cross(dir_phi, H_ap))*direction).sum(1) 113 | 114 | factor = (1j*k)/(4*np.pi*r0) * rayArea * np.exp(1j*(r_vec*ray_pos).sum(1)) 115 | 116 | E_theta = factor*B_theta 117 | E_phi = factor*B_phi 118 | 119 | return E_theta, E_phi 120 | 121 | def simulate(alpha, phi, theta, freq, raysperlam, v, f, bounces): 122 | 123 | lam = (3e8)/(freq) 124 | k = 2*np.pi/lam 125 | tubediam = lam/raysperlam 126 | 127 | #Get polarisation of wave 128 | polX = cosd(phi)*cosd(theta)*cosd(alpha)-sind(phi)*sind(alpha) 129 | polY = sind(phi)*cosd(theta)*cosd(alpha)+cosd(phi)*sind(alpha) 130 | polZ = -sind(theta)*cosd(alpha) 131 | pol = np.array([polX,polY, polZ]).T #initial pol 132 | 133 | #get bounding box 134 | bv,_ = igl.bounding_box(v) 135 | bbcentre = (np.max(bv, axis = 0) + np.min(bv, axis = 0))/2 136 | bbradius = igl.bounding_box_diagonal(v)/2 137 | 138 | 139 | #observation direction 140 | obsX = sind(theta)*cosd( phi ) 141 | obsY = sind(theta)* sind( phi ) 142 | obsZ = cosd(theta) 143 | ray_dir = -np.array([obsX,obsY,obsZ]) 144 | 145 | 146 | #create initial ray pool 147 | #ant_centre = bbcentre - ray_dir*(bbradius+1) 148 | ant_centre = 0 - ray_dir*(bbradius+1) 149 | numrays = int(((bbradius*2))/(tubediam)) 150 | _, up, right = ortho_set(phi, theta) 151 | pool_min = ant_centre - ( right +up ) * bbradius 152 | 153 | up_step = tubediam * up 154 | right_step = tubediam * right 155 | 156 | pool_begin = pool_min + ( up_step + right_step ) / 2.0 157 | 158 | xx, yy = np.meshgrid(np.linspace(0, numrays-1, numrays), np.linspace(0, numrays-1, numrays)) 159 | zz0 = (pool_begin[0] + up_step[0]*xx + right_step[0]*yy).reshape(-1) 160 | zz1 = (pool_begin[1] + up_step[1]*xx + right_step[1]*yy).reshape(-1) 161 | zz2 = (pool_begin[2] + up_step[2]*xx + right_step[2]*yy).reshape(-1) 162 | ray_pos = np.tile([0,0,0,0,ray_dir[0], ray_dir[1], ray_dir[2], 10000],(numrays*numrays,1)) 163 | 164 | ray_pos[:,0] = zz0 165 | ray_pos[:,1] = zz1 166 | ray_pos[:,2] = zz2 167 | 168 | 169 | ray_pos = np.array(ray_pos) 170 | rays = np.float32(ray_pos) 171 | rays = rays.flatten() 172 | 173 | ray_dict = np.array([[0, pol[0], pol[1], pol[2]] for i in range(numrays*numrays)]) #index, distance, pol 174 | 175 | '''THIS IS THE BEGINNING SHOOTING''' 176 | 177 | #setup initial rays 178 | pol = np.tile(pol, (numrays*numrays,1)) 179 | hits = [[0,0,0,0] for i in range(numrays*numrays)] 180 | hits = np.float32(hits) 181 | hits = hits.flatten() 182 | 183 | hits = hits.reshape(numrays*numrays, 4) 184 | 185 | #perform ray trace 186 | res = optix.trace(rays, hits, numrays*numrays)#PASS 2 GPU 187 | assert res == 0 188 | 189 | 190 | 191 | numrays = np.count_nonzero(hits[:, 0]>0) #remove initial rays that did not hit geometry 192 | 193 | ray_pos = ray_pos[hits[:, 0] > 0, :] #update corresponding arrays/dicts 194 | ray_dict = ray_dict[hits[:, 0] > 0, :] 195 | hits = hits[hits[:, 0] > 0, :] 196 | 197 | for b in range(bounces): 198 | hits, ray_pos, ray_dict = shoot_and_record(hits, ray_pos, ray_dict, numrays) 199 | 200 | 201 | dir_phi = np.array([-sind(phi) , cosd(phi), 0]) 202 | dir_theta = np.array([cosd(theta)*cosd(phi), cosd(theta)*sind(phi), -sind(theta)]) 203 | dir_r = np.array([sind(theta)*cosd(phi) , sind(theta)*sind(phi), cosd(theta)]) 204 | 205 | r0 = np.linalg.norm(ant_centre) 206 | 207 | #Perform PO integral 208 | rays_tbc = ray_dict[ray_dict[:, 0] > 0] 209 | ray_pos_tbc = ray_pos[ray_dict[:, 0] > 0] 210 | r_prime = ray_pos_tbc[:,0:3] 211 | ray_pol = rays_tbc[:,1:4] 212 | direction = ray_pos_tbc[:,4:7] 213 | dist = rays_tbc[:,0:1] - r0 #remove initial travel distance 214 | E_theta_comp, E_phi_comp = PO_Integral(r_prime, dist , ray_pol, direction ,tubediam, lam, dir_phi, dir_theta, dir_r, r0) 215 | 216 | E_theta_sum = np.sum(E_theta_comp) 217 | E_phi_sum = np.sum(E_phi_comp) 218 | 219 | 220 | 221 | return E_theta_sum, E_phi_sum, r0 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PO-SBR-Python 2 | A python implementation of shooting and bouncing rays (PO-SBR), accelerated using OptiX. 3 | 4 | Feel free to post an issue or email me at yftan@nus.edu.sg if you have anything to ask. 5 | 6 | # How to use 7 | Place POsolver.py into the working folder, and use the functions. 8 | Examples are given in Test_RCS for RCS, and Test_range for radar range profile. 9 | 10 | # Functions that you really need to care about 11 | 1. ```build(filename)``` takes in the filename of the geometry. Supported file formats inherit from libigl: ```obj, off, stl, wrl, ply, mesh```. Returns vertices and faces ```v,f``` 12 | 2. ```simulate(alpha, phi, theta, freq, raysperlam, v, f)```. This simulates monostatic radar. (receive = transmit angle) 13 | 14 | ```alpha``` is the angle the E vector makes with the theta vector. 0/180 degrees is V pol, 90/270 degrees is H pol. 15 | 16 | ```phi``` is the phi angle of observation/transmission. 17 | 18 | ```theta``` is the theta angle of observation/transmission. 19 | 20 | ```freq``` is the simulation frequency in Hz 21 | 22 | ```raysperlam``` is the number of rays per lambda. 3 would give 9 rays in an area of lambda^2, 4 would give 16, etc. 23 | 24 | ```v,f``` are the vertices and faces obtained through ```build(filename)```. Pass ```v,f``` from ```build``` to these parameters in ```simulate```. 25 | 26 | ```bounces``` are the desired number of max bounces you would like the progam to acheive 27 | 28 | Examples are provided in TestRCS.py, and TestRange.py. 29 | 30 | 31 | # Dependencies 32 | numpy: https://numpy.org/ 33 | libigl: https://libigl.github.io/libigl-python-bindings/ 34 | rtxpy (MODIFIED VERSION): 35 | **Grab the modified rtxpy and follow instructions here:** https://github.com/pingpongballz/rtxpy 36 | Original: https://github.com/makepath/rtxpy 37 | **DO NOT USE THE ORIGINAL rtxpy. The original does NOT take into account of inward or outward mesh normals.** 38 | 39 | 40 | # Results 41 | Simulation on RTX4070 SUPER, i7-14700k 42 | Flat plate at boreside. x-axis: degree(angle). y-axis: RCS(dBm^2) 43 | Flat plate dimensions: 1.5m *1.5m. 44 | Angular step: 45 to 135 degrees, 0.1 degree step 45 | Operating frequency: 3GHz 46 | Time to complete: 4.14 seconds 47 | Theoretical boresight maximum: 38.0dB. Simulated: 38.0dB 48 | ![image](https://github.com/pingpongballz/PO-SBR-Python/assets/74599812/8a49788c-7ac9-4485-8ae6-1fb469643d7c) 49 | 50 | 51 | 52 | Dihedral at boreside. x-axis: degree(angle). y-axis: RCS(dBm^2) 53 | Dihedral dimensions: 1.5m *1.5m plates at 90 degree angles to each other. 54 | Angular step: 45 to 135 degrees degrees, 0.1 degree step 55 | Operating frequency: 3GHz 56 | Time to complete: 7.91 seconds 57 | Theoretical boresight maximum: 41.0dB. Simulated: 41.1dB 58 | ![image](https://github.com/pingpongballz/PO-SBR-Python/assets/74599812/70424d6e-5c71-42d8-8389-ee52f6deb619) 59 | 60 | 61 | 62 | Trihedral at boreside. x-axis: degree(angle). y-axis: RCS(dBm^2) 63 | Trihedral dimensions: 1.5m interior length. 64 | Angular step: 45 to 135 degrees degrees, 0.1 degree step 65 | Operating frequency: 3GHz 66 | Time to complete: 8.55 seconds 67 | Theoretical boresight maximum: 33.3dB. Simulated: 33.2dB 68 | ![image](https://github.com/pingpongballz/PO-SBR-Python/assets/74599812/57f95e3a-95b6-4a2a-805a-888e21fdf004) 69 | 70 | 71 | # References 72 | [1] R. Bhalla and H. Ling, "Image domain ray tube integration formula for the shooting and bouncing ray technique," in Radio Science, vol. 30, no. 5, pp. 1435-1446, Sept.-Oct. 1995. 73 | [2] S. W. Lee, H. Ling and R. Chou, "Ray-tube integration in shooting and bouncing ray method", Microwave and Optical Technology Letters, vol. 1, no. 8, pp. 286-289, October 1988. 74 | 75 | -------------------------------------------------------------------------------- /Test_RCS.py: -------------------------------------------------------------------------------- 1 | import POsolver as PO 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import time 5 | 6 | #Initial Params 7 | filename = "geometries/trihedral.obj" 8 | alpha = 180 #0/180 for V pol; 90/270 for H pol 9 | phi = np.arange(45,135,0.1) #90 10 | theta = 90 11 | freq = 3e9 #np.arange(1e9,4e9,30e6) 12 | raysperlam = 3 13 | bounces = 3 14 | 15 | 16 | v,f = PO.build(filename) 17 | RCS = lambda E_theta,E_phi,r0: 10*np.log10(4*np.pi*r0*r0*((abs(E_theta))**2 + (abs(E_phi))**2)) 18 | arr = [] 19 | 20 | 21 | 22 | 23 | tic = time.time() 24 | for ang in phi: 25 | E1, E2,r0 = PO.simulate(alpha, ang, theta, freq, raysperlam, v, f, bounces) 26 | arr.append(RCS(E1,E2,r0)) 27 | toc = time.time() 28 | print(str(toc-tic) + " seconds") 29 | plt.plot(phi, arr) 30 | plt.show() 31 | -------------------------------------------------------------------------------- /geometries/dihedral.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object dihedral.obj 7 | # 8 | # Vertices: 6 9 | # Faces: 4 10 | # 11 | #### 12 | vn 1.1107206 -1.1107206 0.0000000 13 | v 1.0606600 1.0606600 -0.7500000 14 | vn 2.2214413 0.0000000 0.0000000 15 | v 0.0000000 0.0000000 0.7500000 16 | vn 2.2214415 0.0000000 0.0000000 17 | v 0.0000000 0.0000000 -0.7500000 18 | vn 1.1107208 -1.1107208 0.0000000 19 | v 1.0606600 1.0606600 0.7500000 20 | vn 1.1107206 1.1107206 0.0000000 21 | v -1.0606600 1.0606600 -0.7500000 22 | vn 1.1107208 1.1107208 0.0000000 23 | v -1.0606600 1.0606600 0.7500000 24 | # 6 vertices, 0 vertices normals 25 | 26 | f 1//1 2//2 3//3 27 | f 4//4 2//2 1//1 28 | f 5//5 2//2 3//3 29 | f 6//6 2//2 5//5 30 | # 4 faces, 0 coords texture 31 | 32 | # End of File 33 | -------------------------------------------------------------------------------- /geometries/dihedral_offset.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object dihedral_offset.obj 7 | # 8 | # Vertices: 6 9 | # Faces: 4 10 | # 11 | #### 12 | vn 1.1107208 -1.1107208 0.0000000 13 | v 1.0606600 -0.9393399 -0.7500000 14 | vn 2.2214415 0.0000000 0.0000000 15 | v 0.0000000 -2.0000000 0.7500000 16 | vn 2.2214417 0.0000000 0.0000000 17 | v 0.0000000 -2.0000000 -0.7500000 18 | vn 1.1107209 -1.1107208 0.0000000 19 | v 1.0606600 -0.9393399 0.7500000 20 | vn 1.1107208 1.1107208 0.0000000 21 | v -1.0606600 -0.9393399 -0.7500000 22 | vn 1.1107209 1.1107208 0.0000000 23 | v -1.0606600 -0.9393399 0.7500000 24 | # 6 vertices, 0 vertices normals 25 | 26 | f 1//1 2//2 3//3 27 | f 4//4 2//2 1//1 28 | f 5//5 2//2 3//3 29 | f 6//6 2//2 5//5 30 | # 4 faces, 0 coords texture 31 | 32 | # End of File 33 | -------------------------------------------------------------------------------- /geometries/plate.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object plate.obj 7 | # 8 | # Vertices: 4 9 | # Faces: 2 10 | # 11 | #### 12 | vn 0.0000000 1.5707961 0.0000000 13 | v 0.7500000 0.0000000 -0.7500000 14 | vn 0.0000000 1.5707961 0.0000000 15 | v -0.7500000 0.0000000 0.7500000 16 | vn 0.0000000 1.5707964 0.0000000 17 | v 0.7500000 0.0000000 0.7500000 18 | vn 0.0000000 1.5707964 0.0000000 19 | v -0.7500000 0.0000000 -0.7500000 20 | # 4 vertices, 0 vertices normals 21 | 22 | f 1//1 2//2 3//3 23 | f 4//4 2//2 1//1 24 | # 2 faces, 0 coords texture 25 | 26 | # End of File 27 | -------------------------------------------------------------------------------- /geometries/trihedral.obj: -------------------------------------------------------------------------------- 1 | #### 2 | # 3 | # OBJ File Generated by Meshlab 4 | # 5 | #### 6 | # Object trihedral.obj 7 | # 8 | # Vertices: 4 9 | # Faces: 3 10 | # 11 | #### 12 | vn 0.5553603 -0.0000736 0.9619124 13 | v 1.0606600 0.8660723 -0.6123061 14 | vn -0.5553603 -0.0000736 0.9619124 15 | v -1.0606600 0.8660723 -0.6123061 16 | vn 0.0000000 -0.9070961 2.5650301 17 | v 0.0000000 0.0000000 0.0000000 18 | vn 0.0000000 -0.9069487 0.6412055 19 | v 0.0000000 0.8659316 1.2248110 20 | # 4 vertices, 0 vertices normals 21 | 22 | f 1//1 2//2 3//3 23 | f 4//4 2//2 3//3 24 | f 1//1 4//4 3//3 25 | # 3 faces, 0 coords texture 26 | 27 | # End of File 28 | -------------------------------------------------------------------------------- /test_range.py: -------------------------------------------------------------------------------- 1 | import POsolver as PO 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import time 5 | 6 | #Uncomment/comment geometry u wanna use 7 | #Dihedral at ORIGIN. 8 | #filename = "geometries/dihedral.obj" 9 | #Dihedral at 2m OFFSET. 10 | filename = "geometries/dihedral_offset.obj" 11 | 12 | #Initial Params. 13 | alpha = 180 #0/180 for V pol; 90/270 for H pol 14 | phi = 90 15 | theta = 90 16 | freq = np.arange(1,2,0.01)*1e9 17 | raysperlam = 3 18 | bounces = 2 19 | 20 | 21 | v,f = PO.build(filename) 22 | RCS = lambda E_theta,E_phi: 10*np.log10(4*np.pi*np.abs(E_theta)**2) 23 | arr = [] 24 | 25 | 26 | tic = time.time() 27 | for i in freq: 28 | lam = (3e8)/(i) 29 | k = 2*np.pi/lam 30 | E1, E2,r0 = PO.simulate(alpha, phi, theta, i, raysperlam, v, f, bounces) 31 | arr.append(E1) 32 | 33 | 34 | toc = time.time() 35 | print(str(toc-tic) + " seconds") 36 | arr_fft = np.fft.ifftshift(np.fft.ifft(arr)) #get range profile 37 | plt.plot(np.linspace(-7.5,7.5,len(freq)),np.abs(arr_fft)) 38 | plt.title("0m offset" if filename == "geometries/dihedral.obj" else "2m offset") 39 | plt.show() 40 | 41 | 42 | 43 | --------------------------------------------------------------------------------