def reflect(self, reflect_type="specular_flat", normal=None, optic=None, intersection_pt=None): """ Reflect the ray """ if reflect_type == "specular_flat": i = unit_vector(self.direction) # incident unit vector n = unit_vector(normal) # normal unit vector r = i - 2 * np.dot(i, n) * n # reflected self.direction = unit_vector(r) elif reflect_type == "grating": normal = unit_vector(normal) incident = unit_vector(self.direction) # get only angles in the grating active plane dispersion_plane_normal = optic.tangent n = unit_vector(project_onto_plane(normal, dispersion_plane_normal)) i = unit_vector( project_onto_plane(incident, dispersion_plane_normal)) alpha = angle_between( -i, n, dispersion_plane_normal ) # incident light angle with respect to grating normal (defined as plus) # print(f"dispersion_plane_normal = {dispersion_plane_normal}") # print(f"n = {n}") # print(f"i = {i}") # print(f"alpha = {alpha}") if alpha < 0: grating_sign_convention = -1 else: grating_sign_convention = +1 # the grating equation: G m lambda = sin(alpha) + sin(beta), # G = 1/d = groove density = "grooves per millimeter" # m = order (-1, 0, +1, etc) # d = groove spacing (mm) # sign convention: negative orders are towards the side of zero order # positive orders are backtowards the incident light beta = np.arcsin(optic.G / 1e6 * self.order * self.wavelength - np.sin(grating_sign_convention * alpha)) # theta = n + grating_sign_convention * beta # TODO convert back to ray direction r = incident - 2 * np.dot(incident, normal) * normal # reflected RM1 = rotation_matrix_axis_angle(dispersion_plane_normal, -grating_sign_convention * alpha) # self.direction = unit_vector(RM1.dot(r)) RM2 = rotation_matrix_axis_angle(dispersion_plane_normal, -beta) self.direction = unit_vector(RM2.dot(unit_vector(RM1.dot(r)))) if self.print_trajectory: print("light at wavelength {} turned {} by grating".format( self.wavelength, alpha + beta)) else: raise ValueError(f"Unknown reflect type {reflect_type}")
def __init__(self, center, normal, tangent, h=None, w=None): self.center = center self.normal = unit_vector(normal) self.tangent = unit_vector(tangent) if not np.isclose(np.dot(self.normal, self.tangent), 0.0): raise ValueError("normal and tangent need to be perpendicular") self.tangent2 = np.cross(self.normal, self.tangent) (h, w) = check_h_and_w(h, w) self.h = h self.w = w self.bounds = np.zeros((4, 3)) self.bounds[0, :] = self.center + h / 2 * self.tangent - w / 2 * self.tangent2 self.bounds[1, :] = self.center + h / 2 * self.tangent + w / 2 * self.tangent2 self.bounds[2, :] = self.center - h / 2 * self.tangent + w / 2 * self.tangent2 self.bounds[3, :] = self.center - h / 2 * self.tangent - w / 2 * self.tangent2 self.plane = Plane(center, normal)
def __init__(self, position, normal=None, shape="spherical_biconvex", D=None, w=None, h=None, f=None, tangent=None, type="normal", index=1.5): self.position = position if normal is not None: self.normal = unit_vector(normal) else: self.normal = normal self.shape = shape self.D = D self.w = w self.h = h self.f = f self.surfaces = [] self.type = type self.index = index if shape == "spherical_biconvex": self.surfaces.append(Circle(position, normal, D=D)) elif shape == "spherical": self.surfaces.append(Sphere(position, D=D)) else: raise ValueError("Unknown shape")
def __init__(self, center, direction, R=None, D=None): self.center = center (R, D) = check_R_and_D(R, D) self.R = R self.D = D self.direction = unit_vector(direction) self.plane = Plane(center, normal=direction)
def __init__(self, center, normal=None, R=None, D=None): self.center = center (R, D) = check_R_and_D(R, D) self.R = R self.D = D if normal is not None: self.normal = unit_vector(normal) self.plane = Plane(center, normal)
def __init__(self, position, normal, tangent, shape="rectangular_flat", w=None, h=None): self.position = position self.normal = unit_vector(normal) self.tangent = unit_vector(tangent) self.shape = shape self.w = w self.h = h self.surfaces = [] self.hit_data = [] if shape == "rectangular_flat": self.surfaces.append(Rectangle(position, normal, tangent, h=h, w=w)) else: raise ValueError("Unknown shape")
def __init__(self, position, shape="circular_flat", normal=None, D=None, w=None, h=None, thickness=10, f=None, tangent=None, type="normal", dmd_normal=None): self.position = position self.shape = shape self.D = D self.w = w self.h = h self.thickness = thickness self.f = f self.type = type self.dmd_normal = unit_vector(dmd_normal) self.surfaces = [] if shape == "circular_flat": self.normal = unit_vector(normal) self.surfaces.append(Circle(position, normal, D=D)) elif shape == "rectangular_flat": self.normal = unit_vector(normal) self.surfaces.append(Rectangle(position, normal, tangent, h=h, w=w)) elif shape == "circular_concave_spherical": self.normal = unit_vector(normal) self.rc = self.f * 2 self.p_rc = self.position + self.normal * self.rc self.surfaces.append(Circle(position, normal, D=D)) self.surfaces.append(Sphere(center=self.p_rc, R=self.rc)) elif shape == "circular_convex_spherical": self.surfaces.append(Sphere(center=self.position, D=self.D)) else: raise ValueError("Unknown shape")
def test_intersect(self, ray): # print("*** test intersect ***") # print(f"self.normal = {self.normal}") # print(f"self.center = {self.center}") # print(f"ray_position = {ray.position}") # print(f"ray.direction = {ray.direction}") # print(f"ray z angle = {180 / np.pi * np.arctan(ray.direction[2] / ray.direction[1])} deg") denominator = np.dot(unit_vector(self.normal), unit_vector(ray.direction)) # ARB_EPSILON_VALUE is an arbitrary epsilon value. We just want # to avoid working with intersections that are almost orthogonal. ARB_EPSILON_VALUE = 1e-9 if np.abs(denominator) > ARB_EPSILON_VALUE: difference = self.center - ray.position # print(f"difference = {difference}") # print(f"denominator = {denominator}") t = np.dot(difference, self.normal) / denominator # print(f"t = {t}") if t > ARB_EPSILON_VALUE: intersection_pt = ray.position + t * ray.direction # print(f"intersection_pt = {intersection_pt}") # print("*** end test intersect -- hit***") return True, intersection_pt, self.normal # print("*** end test intersect -- no hit ***") return False, None, None
def __init__(self, position, direction, wavelength=532, order=0, print_trajectory=False, type="normal"): self.position = position self.direction = unit_vector(direction) self.wavelength = wavelength # nm self.order = order # nm self.point_history = [] self.point_history.append(position) self.print_trajectory = print_trajectory self.blocked = False self.type = type if self.print_trajectory: print(self)
def refract(self, refract_type="thin_lens", normal=None, optic=None, intersection_pt=None, eta1=None, eta2=None): """Refract a ray.""" if refract_type == "thin_lens": # should rename to "ideal_image" or something # we will turn the ray as if it were coming from an object at 2f to form a perfect 1:1 image at 2f # if this doesn't obey the thin lens imaging equation everywhere, too bad, I think it's the best we can do # without the optic "cheating" i.e. it being told where the "object plane" is supposed to be # okay, looks like this does work and satisfy the lens equation, nice # # we will assume optic normal and self direction are unit vectors r = intersection_pt - optic.position h = np.linalg.norm(r) # distance from center dot_product1 = np.dot(optic.normal, self.direction) if dot_product1 < 0: the_optic_normal = -optic.normal else: the_optic_normal = optic.normal dot_product = np.dot(the_optic_normal, self.direction) if np.isclose(dot_product, 0.0, rtol=1e-10): # vectors are very nearly orthogonal # do nothing pass elif np.allclose(self.direction, the_optic_normal, rtol=1e-10) or np.allclose(self.direction, -the_optic_normal, rtol=1e-10): # vectors are very nearly parallel # update the ray's direction to go at the focal point focal_point = optic.position + optic.f * the_optic_normal vector_to_image = focal_point - self.position self.direction = vector_to_image / np.linalg.norm( vector_to_image) else: # vectors are neither parallel nor orthogonal # step 1: back-calculate the "object" point at 2f # um, okay, so we want to multiply it by a scalar such that # the projection in the direction of the optic normal is equal to 2f # scalar projection of a onto b is ( a . b ) / norm(b) projection_onto_optic_normal = np.dot(self.direction, the_optic_normal) scale_factor = (2 * optic.f) / projection_onto_optic_normal vector_back_to_2f_object = -self.direction * scale_factor object_point = r + vector_back_to_2f_object # step 2: get the image point as the inverted object point image_point = -object_point # step 3: update the ray's direction to go towards the image point vector_to_image = image_point - r self.direction = vector_to_image / np.linalg.norm( vector_to_image) # # original method, # # from "Thin Lens Ray Tracing", Gatland, 2002 # # equation 10, nbold_doubleprime = nbold - rbold / f # r = intersection_pt - optic.position # self.direction = unit_vector(self.direction - r / optic.f) # # normalization issue -- trying unit_vector-ize the whole thing 2/16/2020 # # this clears up issues related to having a non-normalized ray direction # # now it just seems to be slightly inaccurate... # # or rather, um, it forms non-perfect images # # okay, believing this might be due to use of the paraxial approximation in this thin lens treatment # # which isn't wrong per se, but what if I want an ideal image forming lens # # ... okay no, if the paraxial is executed well, it should form a perfect image ... elif refract_type == "ideal_collimate": # update the ray's direction to go in the direction of the optic normal dot_product1 = np.dot(optic.normal, self.direction) if dot_product1 < 0: the_optic_normal = -optic.normal else: the_optic_normal = optic.normal self.direction = unit_vector(the_optic_normal) elif refract_type == "ideal_focus": # update the ray's direction to go at the focal point dot_product1 = np.dot(optic.normal, self.direction) if dot_product1 < 0: the_optic_normal = -optic.normal else: the_optic_normal = optic.normal focal_point = optic.position + optic.f * the_optic_normal vector_to_image = focal_point - self.position self.direction = vector_to_image / np.linalg.norm(vector_to_image) elif refract_type == "snells_law": # from 2006 internet pdf: # "Reflections and Refractions in Ray Tracing" # by Bram de Greve ([email protected]) # November 13, 2006 # # subscript 1 is the material you are coming from # subscript 2 is the material you are going into # eta is index of refraction # # surface normal should face toward material you are coming from # do_total_internal_reflection = False i = self.direction n = normal # okay # let's just try to flip the normal in here so that it's right to prevent all these issues # I see no way this could go poorly # so, the angle between the ray direction and the normal should be more than 90 right? that's what it is? # If the angle between A and B are greater than 90 degrees, the dot product will be negative, if not flip n if np.dot(i, n) < 0: pass else: n = -n cos_theta_incident = np.dot(-i, n) sin_squared_theta_transmitted = (eta1 / eta2)**2 * ( 1 - cos_theta_incident**2) if eta1 > eta2: if sin_squared_theta_transmitted > 1: # eq. 24 do_total_internal_reflection = True self.reflect(reflect_type="specular_flat", normal=normal, intersection_pt=intersection_pt) # print("TIR: " + str(do_total_internal_reflection)) if not do_total_internal_reflection: t = (eta1 / eta2) * i + ( (eta1 / eta2) * cos_theta_incident - np.sqrt(1 - sin_squared_theta_transmitted)) * n self.direction = t else: raise ValueError("Unrecognized refract_type input ") if self.print_trajectory: print(self)
def __init__(self, position, normal, shape): self.position = position self.normal = unit_vector(normal) self.shape = shape
def __init__(self, center, normal): self.center = center self.normal = unit_vector(normal)
def normal(self, point): """The surface normal at the given point on the sphere, pointing toward the center""" return unit_vector(self.center - point)
def normal(self, point): """The surface normal at the given point on the infinite cylinder, pointing toward the interior""" r = self.center - point; n = project_onto_plane(r, self.direction) return unit_vector(n)