def test_normal_refraction(self):
     node = Node(name="node")
     ctx = Context(n1=1.0, n2=1.5, normal_node=node, normal=(0.0, 0.0, 1.0), kind=Kind.SURFACE, end_path=(10, 10, 10), container=None)
     ray = Ray(position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), wavelength=None)
     interaction = FresnelRefraction(ctx)
     new_ray = interaction.transform(ray)
     assert ray == new_ray
Example #2
0
 def __init__(self, refractive_index: np.ndarray, lumophores: [Lumophore]):
     super(Host, self).__init__(
         refractive_index=refractive_index,
         lumophores=lumophores
     )
     self._transit_mechanism = FresnelRefraction()
     self._return_mechanism = FresnelReflection()
     self._path_mechanism = Absorption()
     self._emit_mechanism = Emission()
Example #3
0
 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 = FresnelRefraction()
     new_ray = fresnel.transform(ray, {
         "n1": n1,
         "n2": n2,
         "normal": normal
     })
     assert np.allclose(ray.direction, new_ray.direction)
Example #4
0
 def __init__(self, refractive_index: np.ndarray,
              absorption_coefficient: np.ndarray):
     super(LossyDielectric, self).__init__(absorption_coefficient,
                                           refractive_index)
     self._transit_mechanism = FresnelRefraction()
     self._return_mechanism = FresnelReflection()
     self._path_mechanism = Absorption()
     self._emit_mechanism = None
Example #5
0
 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 = FresnelRefraction()
     new_ray = fresnel.transform(ray, {
         "n1": n1,
         "n2": n2,
         "normal": normal
     })
     assert np.allclose(ray.direction, new_ray.direction)
Example #6
0
 def __init__(self, refractive_index):
     super(Dielectric, self).__init__(refractive_index)
     self._transit_mechanism = FresnelRefraction()
     self._return_mechanism = FresnelReflection()
     self._path_mechanism = TravelPath()
     self._emit_mechanism = None
Example #7
0
class Dielectric(Refractive, Material):
    """ A material with a refractive index.
    
        Notes
        -----
        The material is unphysical in the sense that it does not absorb or emit light. 
        But it is useful in development and testing to have material which just 
        interacts with ray without in a purely refractive way.

    """
    def __init__(self, refractive_index):
        super(Dielectric, self).__init__(refractive_index)
        self._transit_mechanism = FresnelRefraction()
        self._return_mechanism = FresnelReflection()
        self._path_mechanism = TravelPath()
        self._emit_mechanism = None

    def trace_path(self, local_ray: "Ray", container_geometry: "Geometry",
                   distance: float):
        """ Dielectric material does not have any absorption; this moves ray full dist.
        """
        new_ray = self._path_mechanism.transform(local_ray,
                                                 {"distance": distance})
        yield new_ray, Decision.TRAVEL

    def trace_surface(
        self,
        local_ray: "Ray",
        container_geometry: "Geometry",
        to_geometry: "Geometry",
        surface_geometry: "Geometry",
    ) -> Tuple[Decision, dict]:
        """ 
        """
        # Get reflectivity for the ray
        normal = surface_geometry.normal(local_ray.position)
        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

    def trace_surface(
        self,
        local_ray: "Ray",
        container_geometry: "Geometry",
        to_geometry: "Geometry",
        surface_geometry: "Geometry",
    ) -> Tuple[Decision, dict]:
        """ 
        """
        # Get reflectivity for the ray
        normal = surface_geometry.normal(local_ray.position)
        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

    @classmethod
    def make_constant(cls, x_range: Tuple[float, float],
                      refractive_index: float):
        """ Returns a dielectric material with spectrally constant refractive index.

        """
        refractive_index = np.column_stack(
            (x_range, [refractive_index, refractive_index]))
        return cls(refractive_index)

    @classmethod
    def air(cls, x_range: Tuple[float, float] = (300.0, 4000.0)):
        """ Returns a dielectric material with constant refractive index of 1.0 in
            default range.

        """
        return cls.make_constant(x_range=x_range, refractive_index=1.0)

    @classmethod
    def glass(cls, x_range: Tuple[float, float] = (300.0, 4000.0)):
        """ Returns a dielectric material with constant refractive index of 1.5 in
            default range.

        """
        return cls.make_constant(x_range=x_range, refractive_index=1.5)
Example #8
0
 def test_init(self):
     assert type(FresnelRefraction()) == FresnelRefraction
Example #9
0
class Host(Refractive, Blend, Material):
    """ A material with a refractive index that can host a single or multiple
        Lumophores.
    """

    def __init__(self, refractive_index: np.ndarray, lumophores: [Lumophore]):
        super(Host, self).__init__(
            refractive_index=refractive_index,
            lumophores=lumophores
        )
        self._transit_mechanism = FresnelRefraction()
        self._return_mechanism = FresnelReflection()
        self._path_mechanism = Absorption()
        self._emit_mechanism = Emission()

    def select_lumophore(self, nanometers: float) -> Lumophore:
        """ Selects, at random, one of the lumophores from the list.
        
            Parameters
            ----------
            nanometers : float
                The wavelength of the interacting photon in nanometers.

            Returns
            -------
            Lumophore
                The lumophore which absorbed the photon.

            Notes
            -----
            The selection is weighted by the relative absorption strength of all
            materials at the given wavelength.
        """
        absorptions = [l.absorption_coefficient(nanometers) for l in self.lumophores]
        count = len(self.lumophores)
        bins = list(range(0, count + 1))
        cdf = np.cumsum(absorptions)
        pdf = cdf / max(cdf)
        pdf = np.hstack([0, pdf[:]])
        pdfinv_lookup = np.interp(np.random.uniform(), pdf, bins)
        absorber_index = int(np.floor(pdfinv_lookup))
        lumophore = self.lumophores[absorber_index]
        return lumophore

    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

    def trace_path(
            self, 
            local_ray: "Ray",
            container_geometry: "Geometry",
            distance: float
    ) -> Tuple[Decision, dict]:
                
        # Which of the host's materials captured the ray
        material = self.select_lumophore(local_ray.wavelength)
        # Sample the exponential distribution and get a distance at which the
        # ray is absorbed.
        sampled_distance = self._path_mechanism.path_length(
            local_ray.wavelength, material
        )
        logger.debug("Host.trace_path args: {}".format((local_ray, container_geometry, distance)))
        # If the sampled distance is less than the full distance the ray can travel
        # then the ray is absorbed.
        if sampled_distance < distance:
            # Apply the absorption transformation to the ray; this updates the rays
            # position to the absorption location.
            info = {"distance": sampled_distance}
            #print("Sampled pathlength: {}".format(info))
            new_ray = self._path_mechanism.transform(local_ray, info)
            # Test if ray is reemitted by comparing a random number to the quantum yield
            qy = material.quantum_yield
            # If the random number is less than the quantum yield then emission occurs.
            if np.random.uniform() < qy:
                # If ray is re-emitted generate two events: ABSORB and EMIT
                yield new_ray, Decision.ABSORB
                # Emission occurred
                new_ray = self._emit_mechanism.transform(new_ray, {"material": material})
                yield new_ray, Decision.EMIT
            else:
                # If the ray is not emitted generate one event: ABSORB
                # Non-radiative absorption occurred
                new_ray = replace(new_ray, is_alive=False)
                yield new_ray, Decision.ABSORB
        else:
            # If not absorbed travel the full distance
            info = {"distance": distance}
            new_ray = self._path_mechanism.transform(local_ray, info)
            yield new_ray, Decision.TRAVEL

    @classmethod
    def from_dataframe(cls, df):
        """ Returns a Host instance initialised with data from the dataframe.
        
            Parameters
            ----------
                df : pandas.DataFrame
            The dataframe must start with the following columns (an index column is 
            fine and will be ignored),
                - *wavelength <anything>*, the wavelength in nanometers
                - *refractive index <anything>*, the real part of the refractive index
            Then the subsequent columns must be absorption coefficient, emission
            spectrum, and quantum yield pairs for the individual lumophores that are
            added to the host,
                - *absorption coefficient <anything>*, the absorption coefficient of the
                lumophore in units of 1/cm
                - *emission spectrum <anything>*, the emission spectrum of lumophore in
                arbitrary units (just the shape is important).
                - *quantum yield <anything>*, the quantum yield of the lumophore. Note
                that the wavelength dependence of the quantum yield is *not* used. The
                first value in the column is used as the quantum yield.
            To validate the dataframe only the start of the column names are checked
            so anything after the required part is ignored and can be what you like
            so that you can organise your data.
        """
        columns = df.columns.tolist()
        if tuple(columns[0:2]) != ("wavelength", "refractive index"):
            raise AppError(
                "Data frame is wrong format. The first two column names "
                "be 'wavelength' and 'refractive index'."
            )
        wavelength = df["wavelength"].values
        ior = df["refractive index"].values

        col_err_msg = (
            "Column {} in data frame has incorrect name. Got {} and "
            "expected it to start with {}."
        )
        lumo_cols = columns[2:]
        for idx, name in enumerate(lumo_cols):
            name = name.lower()
            col_idx = idx + 2
            if idx % 3 == 0:
                expected = "absorption coefficient"
                if not name.startswith(expected):
                    raise AppError(col_err_msg.format(col_idx, name, expected))
            elif idx % 3 == 1:
                expected = "emission spectrum"
                if not name.startswith(expected):
                    raise AppError(col_err_msg.format(col_idx, name, expected))
            elif idx % 3 == 2:
                expected = "quantum yield"
                if not name.startswith(expected):
                    raise AppError(col_err_msg.format(col_idx, name, expected))

        if len(lumo_cols) % 3 != 0:
            raise AppError(
                "Data column(s) missing. Columns after the first two should "
                "be groups like (absorption coefficient, emission_spectrum "
                ", quantum_yield)."
            )
        refractive_index = np.column_stack((wavelength, ior))
        from itertools import zip_longest

        def grouper(iterable):
            args = [iter(iterable)] * 3
            return zip_longest(*args)

        lumophores = []
        for (alpha_name, emission_name, qy_name) in grouper(lumo_cols):
            absorption_coefficient = np.column_stack(
                (wavelength, df[alpha_name].values)
            )
            emission_spectrum = np.column_stack((wavelength, df[emission_name].values))
            quantum_yield = df[qy_name].values[0]
            lumo = Lumophore(absorption_coefficient, emission_spectrum, quantum_yield)
            logger.debug(
                "Making lumophore with max absorption coefficient {}".format(
                    np.max(absorption_coefficient[:, 1])
                )
            )
            lumophores.append(lumo)

        host = cls(refractive_index, lumophores)
        return host