optics_base.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import vedo
  2. import numpy as np
  3. vedo.settings.use_depth_peeling = True
  4. ############################
  5. class OpticalElement:
  6. # A base class
  7. def __init__(self):
  8. self.name = "OpticalElement"
  9. self.type = "undefined"
  10. self.normals = []
  11. self._hits = []
  12. self._hits_type = [] # +1 if ray is entering, -1 if exiting
  13. self.cellids = []
  14. def n_at(self, wave_length):
  15. # to be overridden to implement dispersion
  16. return self.ref_index
  17. @property
  18. def hits(self):
  19. """Ray coordinates hitting this element"""
  20. return np.array(self._hits)
  21. @property
  22. def hits_type(self):
  23. """Flag +1 if ray is entering, -1 if exiting"""
  24. return np.array(self._hits_type)
  25. class Lens(vedo.Mesh, OpticalElement):
  26. """A refractive object of arbitrary shape defined by an arbitrary mesh"""
  27. def __init__(self, obj, ref_index="glass"):
  28. vedo.Mesh.__init__(self, obj.dataset, "blue8", 0.5)
  29. OpticalElement.__init__(self)
  30. self.name = obj.name
  31. self.type = "lens"
  32. self.compute_normals(cells=True, points=False)
  33. self.lighting('off')
  34. self.normals = self.celldata["Normals"]
  35. self.ref_index = ref_index
  36. def n_at(self, wave_length): # in meters
  37. """This is where material dispersion law is implemented"""
  38. if self.ref_index == "glass":
  39. # Dispersion of a common borosilicate glass, see:
  40. # https://en.wikipedia.org/wiki/Sellmeier_equation
  41. B1 = 1.03961212
  42. B2 = 0.231792344
  43. C1 = 6.00069867e-03
  44. C2 = 2.00179144e-02
  45. l2 = (wave_length*1e+06)**2
  46. n = np.sqrt(1 + B1 * l2/(l2-C1) + B2 * l2/(l2-C2))
  47. return n
  48. return self.ref_index
  49. class Mirror(vedo.Mesh, OpticalElement):
  50. """A mirror surface defined by an arbitrary Mesh"""
  51. def __init__(self, obj):
  52. vedo.Mesh.__init__(self, obj.dataset, "blue8", 0.5)
  53. OpticalElement.__init__(self)
  54. self.compute_normals(cells=True, points=True)
  55. self.name = obj.name
  56. self.type = "mirror"
  57. self.normals = self.celldata["Normals"]
  58. self.color('silver').lw(0).wireframe(False).alpha(1).phong()
  59. class Screen(vedo.Grid, OpticalElement):
  60. """A simple read out screen plane"""
  61. def __init__(self, sizex, sizey):
  62. vedo.Grid.__init__(self, res=[1,1], s=[sizex,sizey])
  63. # self.triangulate()
  64. OpticalElement.__init__(self)
  65. self.compute_normals(cells=True, points=False)
  66. self.name = "Screen"
  67. self.type = "screen"
  68. self.normals = self.celldata["Normals"]
  69. self.color('red3').lw(2).lighting('off').wireframe(False).alpha(0.2)
  70. class Absorber(vedo.Grid, OpticalElement):
  71. """A simple non detecting absorber, not generating a hit."""
  72. def __init__(self, sizex, sizey):
  73. vedo.Grid.__init__(self, res=[100,100], s=[sizex,sizey])
  74. OpticalElement.__init__(self)
  75. self.compute_normals()
  76. self.name = "Absorber"
  77. self.type = "screen"
  78. self.normals = self.celldata["Normals"]
  79. self.color('k3').lw(1).lighting('default').wireframe(False).alpha(0.8)
  80. class Detector(vedo.Mesh, OpticalElement):
  81. """A detector surface defined by an arbitrary Mesh"""
  82. def __init__(self, obj):
  83. vedo.Mesh.__init__(self, obj.dataset, "k5", 0.5)
  84. OpticalElement.__init__(self)
  85. self.compute_normals()
  86. self.name = "Detector"
  87. self.type = "screen"
  88. self.normals = self.celldata["Normals"]
  89. self.color('k9').lw(2).lighting('off').wireframe(False).alpha(1)
  90. def count(self):
  91. """Count the hits on the detector cells and store them in cell array 'Counts'."""
  92. arr = np.zeros(self.ncells, dtype=np.uint)
  93. for cid in self.cellids:
  94. arr[cid] += 1
  95. self.celldata["Counts"] = arr
  96. return self
  97. def integrate(self, pols):
  98. """Integrate the polarization vector and store
  99. the probability in cell array 'Probability'."""
  100. arr = np.zeros([self.ncells, 3], dtype=float)
  101. for i, cid in enumerate(self.cellids):
  102. arr[cid] += pols[i]
  103. arr = np.power(np.linalg.norm(arr, axis=1), 2) / len(self.cellids)
  104. self.celldata["Probability"] = arr
  105. return self
  106. ###################################################
  107. class Ray:
  108. """A photon to be tracked as a ray of light.
  109. wave_length in meters (so use e.g. 450.0e-09 m = 450 nm)"""
  110. def __init__(self, origin=(0,0,0), direction=(0,0,1),
  111. wave_length=450.0e-09, phase=0, pol=(1,0,0), n=1.003):
  112. self.name = "Ray"
  113. self.p = np.asarray(origin) # current position
  114. self.v = np.asarray(direction)
  115. self.v = self.v / np.linalg.norm(self.v)
  116. self.wave_length = wave_length
  117. self.path = [self.p]
  118. self._amplitudes = [1.0]
  119. self._polarizations = [np.array(pol)]
  120. self.phase = phase
  121. self.dmax = 20
  122. self.maxiterations = 20
  123. self.tolerance = None # will be computed automatically
  124. self.OBBTreeTolerance = 1e-05 # None = automatic
  125. self.ref_index = n
  126. @property
  127. def amplitudes(self):
  128. """
  129. Amplitudes/attenuations at each hit.
  130. It assumes random light polarization (natural light).
  131. """
  132. return np.array(self._amplitudes)
  133. @property
  134. def polarizations(self):
  135. """Exact polarization vector at each hit."""
  136. return np.array(self._polarizations)
  137. def _rotate(self, p, angle, axis):
  138. magv = np.linalg.norm(axis)
  139. if not magv: return p
  140. a = np.cos(angle / 2)
  141. b, c, d = -axis * (np.sin(angle / 2) /magv)
  142. aa, bb, cc, dd = a * a, b * b, c * c, d * d
  143. bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d
  144. R = np.array([ [aa + bb - cc - dd, 2 * (bc + ad), 2 * (bd - ac)],
  145. [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)],
  146. [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc] ])
  147. return np.dot(R, p)
  148. def _reflectance(self, r12, theta_i, theta_t, inout):
  149. """Fresnel law for probability to reflect at interface, r12=n1/n2.
  150. This can be used to compute how much of the main ray arrives at the screen.
  151. A list of amplitudes at each step is stored in ray.aplitudes.
  152. """
  153. if inout < 0: # need to check the sign
  154. ci = np.cos(theta_i)
  155. ct = np.cos(theta_t)
  156. else: # flip
  157. ct = np.cos(theta_i)
  158. ci = np.cos(theta_t)
  159. r12 = 1 / r12
  160. a = (r12*ci - ct) / (r12*ci + ct) # Rs
  161. b = (r12*ct - ci) / (r12*ct + ci) # Rp
  162. return (a*a + b*b)/2
  163. # def intersect(self, element, p0,p1): # not working (but no need to)
  164. # points = element.points
  165. # faces = element.cells
  166. # cids = []
  167. # for i,f in enumerate(faces):
  168. # v0,v1,v2 = points[f]
  169. # res = vedo.utils.intersection_ray_triangle(p0,p1, v0,v1,v2)
  170. # if res is not False:
  171. # if res is not None:
  172. # cids.append([res, i])
  173. # return cids
  174. def trace(self, elements):
  175. """Trace the path of a single photon through the input list of lenses, mirrors etc."""
  176. for element in elements:
  177. self.tolerance = element.diagonal_size()/1000.
  178. for _ in range(self.maxiterations):
  179. hits, cids = element.intersect_with_line( # faster
  180. self.p,
  181. self.p + self.v * self.dmax,
  182. return_ids=True,
  183. tol=self.OBBTreeTolerance,
  184. )
  185. # hit_cids = self.intersect(element, self.p, self.p + self.v * self.dmax)
  186. if len(hits) == 0:
  187. break # no hits
  188. hit, cid = hits[0], cids[0] # grab the first hit, point and cell ID of the mesh
  189. d = np.linalg.norm(hit - self.p)
  190. if d < self.tolerance:
  191. # it's picking itself.. get the second hit if it exists
  192. if len(hits) < 2:
  193. break
  194. hit, cid = hits[1], cids[1]
  195. d = np.linalg.norm(hit - self.p)
  196. n = element.normals[cid]
  197. w = np.cross(self.v, n)
  198. sintheta1 = np.linalg.norm(w)
  199. theta1 = np.arcsin(sintheta1)
  200. inout = np.sign(np.dot(n, self.v)) # ray entering of exiting
  201. ref_index = self.ref_index
  202. # polarization vector
  203. k = 2*np.pi / (self.wave_length/ref_index)
  204. pol = self._polarizations[-1]
  205. amp = self._amplitudes[-1] # this assumes random polarizations
  206. hit_type = -3
  207. if element.type == "screen":
  208. if element.name == "Wall":
  209. break
  210. pol = pol * np.cos(k * d + self.phase)
  211. elif element.type == "mirror":
  212. theta1 *= inout # mirrors must reflect on both sides
  213. self.v = self._rotate(-self.v, 2*theta1, w)
  214. pol = pol * np.cos(k * d + self.phase + np.pi)
  215. hit_type = -2
  216. elif element.type == "lens":
  217. ref_index = element.n_at(self.wave_length) # dispersion
  218. r = ref_index/self.ref_index if inout>0 else self.ref_index/ref_index
  219. sintheta2 = r * sintheta1 # Snell law
  220. if abs(sintheta2) > 1.0: # total internal reflection
  221. self.v = self._rotate(-self.v, 2*theta1, w)
  222. pol = pol * np.cos(k * d + self.phase + np.pi)
  223. hit_type = -2
  224. else: # refraction
  225. theta2 = np.arcsin(sintheta2)
  226. self.v = self._rotate(self.v, theta2-theta1, -w*inout)
  227. amp = amp * (1-self._reflectance(r, theta1, theta2, inout))
  228. pol = pol * np.cos(k * d + self.phase)
  229. hit_type = -inout
  230. else:
  231. print("Unknown element type", element.type)
  232. self._amplitudes.append(amp)
  233. self._polarizations.append(pol)
  234. self.path.append(hit)
  235. element._hits.append(hit)
  236. element._hits_type.append(hit_type)
  237. element.cellids.append(cid)
  238. if element.type == "screen":
  239. break
  240. self.p = hit # update position
  241. self.path = np.array(self.path)
  242. return self
  243. def asLine(self, min_hits=1, max_hits=1000, c=None, cmap_amplitudes="", vmin=0):
  244. """Return a vedo.Line object if it has at least min_hits and less than max_hits"""
  245. if min_hits < len(self.path) < max_hits:
  246. ln = vedo.Line(self.path).lw(1)
  247. if cmap_amplitudes:
  248. ln.cmap(cmap_amplitudes, self._amplitudes, vmin=vmin)
  249. elif c is None:
  250. c = vedo.colors.color_map(self.wave_length, "jet", 450e-09, 750e-09) /1.5
  251. ln.color(c)
  252. else:
  253. ln.color(c)
  254. return ln
  255. return None