def probability(self, ray: Ray) -> float: """ Returns the probability that the interaction will occur. """ context = self.context normal = np.array(context.normal) n1 = context.n1 n2 = context.n2 # Be flexible with how the normal is defined ray_ = ray.representation(context.normal_node.root, context.normal_node) if np.dot(normal, ray_.direction) < 0.0: normal = flip(normal) angle = angle_between(normal, np.array(ray_.direction)) logger.debug("Incident angle {:.2f}".format(np.degrees(angle))) if angle < 0.0 or angle > 0.5 * np.pi: raise ValueError("The incident angle must be between 0 and pi/2.") # Catch TIR case if n2 < n1 and angle > np.arcsin(n2/n1): return 1.0 c = np.cos(angle) s = np.sin(angle) k = np.sqrt(1 - (n1/n2 * s)**2) Rs1 = n1 * c - n2 * k Rs2 = n1 * c + n2 * k Rs = (Rs1/Rs2)**2 Rp1 = n1 * k - n2 * c Rp2 = n1 * k + n2 * c Rp = (Rp1/Rp2)**2 return 0.5 * (Rs + Rp)
def transform(self, ray: Ray, context: dict) -> Ray: """ Transform ray according to the physics of the interaction. Parameters ---------- ray : Ray The ray undergoing the transformation. context : dict Dictionary containing information of the calculation. Raises ------ KeyError If the context dictionary has missing keys. Notes ----- The ray and any position or direction vectors in the context dictionary must be in the same coordinate system. This method required the following items in the dictionary, normal : 3-tuple of floats The normal vector of the surface """ required = set(["normal"]) _check_required_keys(required, context) # Must be in the local frame normal = np.array(context["normal"]) if np.dot(normal, ray.direction) < 0.0: normal = flip(normal) vec = np.array(ray.direction) d = np.dot(normal, vec) reflected_direction = vec - 2 * d * normal new_position = np.array( ray.position) + 2 * EPS_ZERO * np.array(flip(normal)) new_ray = replace(ray, position=new_position, direction=tuple(reflected_direction.tolist())) return new_ray
def test_normal_reflection(self): n1 = 1.0 n2 = 1.5 normal = (0.0, 0.0, 1.0) angle = 0.0 ray = Ray(position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), wavelength=None) fresnel = FresnelReflection() assert np.isclose(fresnel.reflectivity(angle, n1, n2), 0.04) new_ray = fresnel.transform(ray, {"normal": normal}) assert np.allclose(flip(ray.direction), new_ray.direction)
def test_antinormal_reflection(self): """ FresnelReflection takes the smallest angle between the ray direction and the normal. Thus the flipped normal will also work. """ n1 = 1.0 n2 = 1.5 normal = (0.0, 0.0, -1.0) angle = 0.0 ray = Ray(position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), wavelength=None) fresnel = FresnelReflection() assert np.isclose(fresnel.reflectivity(angle, n1, n2), 0.04) new_ray = fresnel.transform(ray, {"normal": normal}) assert np.allclose(flip(ray.direction), new_ray.direction)
def transform(self, ray: Ray, context: dict) -> Ray: """ Transform ray according to the physics of the interaction. Parameters ---------- ray : Ray The ray undergoing the transformation. context : dict Dictionary containing information of the calculation. Notes ----- The ray and any position or direction vectors in the context dictionary must be in the same coordinate system. This method required the following items in the dictionary, normal : 3-tuple of floats The normal vector of the surface. n1 : float The refractive index of the origin material. n2: float The refractive index of the destination material. """ required = set(["normal", "n1", "n2"]) _check_required_keys(required, context) normal = np.array(context["normal"]) if np.dot(normal, ray.direction) < 0.0: normal = flip(normal) n1 = context["n1"] n2 = context["n2"] vector = np.array(ray.direction) n = n1 / n2 dot = np.dot(vector, normal) c = np.sqrt(1 - n**2 * (1 - dot**2)) sign = 1 if dot < 0.0: sign = -1 refracted_direction = n * vector + sign * (c - sign * n * dot) * normal # maybe not best to use refracted direction; better to use normal because it # moves a constant distance from the surface everytime? new_position = np.array(ray.position) + 2 * EPS_ZERO * np.array(normal) new_ray = replace(ray, position=tuple(new_position.tolist()), direction=tuple(refracted_direction.tolist())) return new_ray
def trace_surface( self, local_ray: "Ray", container_geometry: "Geometry", to_geometry: "Geometry", surface_geometry: "Geometry", ) -> Tuple[Decision, dict]: """ """ # Ray both materials need a refractive index to compute Frensel reflection; # if they are not the both refractive then just let ray cross the interface. try: normal = surface_geometry.normal(local_ray.position) except Exception: import pdb; pdb.set_trace() if not all([isinstance(x, Refractive) for x in (container_geometry.material, to_geometry.material)]): new_ray = CrossInterface().transform(local_ray, {"normal": normal}) yield new_ray, Decision.TRANSIT return # Get reflectivity for the ray n1 = container_geometry.material.refractive_index(local_ray.wavelength) n2 = to_geometry.material.refractive_index(local_ray.wavelength) # Be flexible with how the normal is defined if np.dot(normal, local_ray.direction) < 0.0: normal = flip(normal) angle = angle_between(normal, np.array(local_ray.direction)) if angle < 0.0 or angle > 0.5 * np.pi: raise TraceError("The incident angle must be between 0 and pi/2.") incident = local_ray.direction reflectivity = self._return_mechanism.reflectivity(angle, n1, n2) #print("Reflectivity: {}, n1: {}, n2: {}, angle: {}".format(reflectivity, n1, n2, angle)) gamma = np.random.uniform() info = {"normal": normal, "n1": n1, "n2": n2} # Pick between reflection (return) and transmission (transit) if gamma < reflectivity: new_ray = self._return_mechanism.transform(local_ray, info) decision = Decision.RETURN yield new_ray, decision else: new_ray = self._transit_mechanism.transform(local_ray, info) decision = Decision.TRANSIT yield new_ray, decision