Пример #1
0
class AiryFWHMPatternData(RadialPatternData):
    """
    A description of the properties of an airy pattern.
    """

    amplitude: Union[float, Multivalue[float]] = extended_field(
        1.0, description="amplitude")
    fwhm: Union[float,
                Multivalue[float]] = extended_field(240.0,
                                                    description="FWHM [nm]")

    # Methods
    def get_numpy_array(self,
                        canvas_inner_radius_nm: float,
                        px_size_nm: float,
                        extend_sides_to_diagonal: bool = False) -> np.ndarray:
        return generate_airy(amplitude=self.amplitude,
                             fwhm=self.fwhm,
                             canvas_radius=get_canvas_radius_nm(
                                 canvas_inner_radius_nm,
                                 extend_sides_to_diagonal),
                             px_size_nm=px_size_nm)

    def __str__(self) -> str:
        return f"Airy; amplitude = {self.amplitude}, FWHM = {self.fwhm} nm"
Пример #2
0
class IlluminationResponse:
    """
    A description of the illumination response of a fluorophore at a certain wavelength.
    """

    wavelength: float = extended_field(observable_property(
        "_wavelength", default=0.0, signal_name="basic_field_changed"),
                                       description="wavelength [nm]")

    cross_section_off_to_on: Union[float, Multivalue[float]] = extended_field(
        observable_property("_cross_section_off_to_on",
                            default=0.0,
                            signal_name="basic_field_changed"),
        description="ON cross section",
        accept_multivalues=True)

    cross_section_on_to_off: Union[float, Multivalue[float]] = extended_field(
        observable_property("_cross_section_on_to_off",
                            default=0.0,
                            signal_name="basic_field_changed"),
        description="OFF cross section",
        accept_multivalues=True)

    cross_section_emission: Union[float, Multivalue[float]] = extended_field(
        observable_property("_cross_section_emission",
                            default=0.0,
                            signal_name="basic_field_changed"),
        description="emission cross section",
        accept_multivalues=True)

    # Methods
    def __str__(self) -> str:
        return f"{self.wavelength} nm"
Пример #3
0
class DoughnutPatternData(RadialPatternData):
    """
    A description of the properties of a doughnut pattern.
    """

    periodicity: Union[float, Multivalue[float]] = extended_field(
        480.0, description="periodicity [nm]")

    zero_intensity: Union[float, Multivalue[float]] = extended_field(
        0.0, description="relative intensity in min. [%]")

    # Methods
    def get_numpy_array(self,
                        canvas_inner_radius_nm: float,
                        px_size_nm: float,
                        extend_sides_to_diagonal: bool = False) -> np.ndarray:
        return generate_doughnut(periodicity=self.periodicity,
                                 zero_intensity=self.zero_intensity,
                                 canvas_radius=get_canvas_radius_nm(
                                     canvas_inner_radius_nm,
                                     extend_sides_to_diagonal),
                                 px_size_nm=px_size_nm)

    def __str__(self) -> str:
        return f"Doughnut;" \
               f" periodicity = {self.periodicity} nm," \
               f" relative intensity in min. = {self.zero_intensity}%"
Пример #4
0
class DetectorProperties:
    """
    A description of a detector.
    """

    readout_noise: Union[float, Multivalue[float]] = extended_field(
        observable_property("_readout_noise", default=0.0, signal_name="basic_field_changed"),
        description="readout noise [rms]", accept_multivalues=True
    )

    quantum_efficiency: Union[float, Multivalue[float]] = extended_field(
        observable_property("_quantum_efficiency", default=0.75, signal_name="basic_field_changed"),
        description="quantum efficiency", accept_multivalues=True
    )

    camera_pixel_size: Optional[Union[float, Multivalue[float]]] = extended_field(
        # nanometres (if point detector: None)
        observable_property("_camera_pixel_size", default=None, signal_name="basic_field_changed"),
        description="camera pixel size [nm]", accept_multivalues=True
    )

    def get_total_readout_noise_var(self, canvas_inner_radius_nm: float,
                                    pinhole_function: Pattern) -> float:
        """ Returns the total readout noise of the detector as a variance. """
        if self.camera_pixel_size is not None:
            pinhole_sum = (
                    pinhole_function.get_numpy_array(
                        canvas_inner_radius_nm, self.camera_pixel_size
                    ) ** 2
            ).sum()

            return pinhole_sum * self.readout_noise ** 2
        else:
            return self.readout_noise ** 2
class ExplicitSampleProperties:
    """
    An explicit description of sample-related properties in an environment.
    """

    input_power: Union[float, Multivalue[float]] = extended_field(
        observable_property("_input_power",
                            default=1.0,
                            signal_name="basic_field_changed"),
        description="input power",
        accept_multivalues=True)

    D_origin: Union[float, Multivalue[float]] = extended_field(
        observable_property("_D_origin",
                            default=1.0,
                            signal_name="basic_field_changed"),
        description="D(0, 0)",
        accept_multivalues=True)
class ImagingSystemSettings:
    """
    A description of an imaging system.
    """

    optical_psf: Pattern = extended_field(default_factory=Pattern,
                                          description="optical PSF")

    pinhole_function: Pattern = extended_field(default_factory=Pattern,
                                               description="pinhole function")

    scanning_step_size: Union[float, Multivalue[float]] = extended_field(
        observable_property("_scanning_step_size", default=10.0, signal_name="basic_field_changed"),
        description="scanning step size [nm]", accept_multivalues=True
    )

    refractive_index: float = observable_property(
        "_refractive_index", default=RefractiveIndex.oil.value, signal_name="basic_field_changed"
    )
class SampleProperties:
    """
    A description of sample-related properties in an environment.
    """

    basic_properties: ExplicitSampleProperties = extended_field(
        observable_property("_basic_properties",
                            default=ExplicitSampleProperties(),
                            signal_name="data_loaded"),
        description="sample")

    structure: Optional[SampleStructure] = extended_field(
        observable_property("_structure",
                            default=None,
                            signal_name="data_loaded"),
        description="structure")

    def load_structure(self, structure: SampleStructure) -> None:
        """ Loads a sample structure. """
        self.basic_properties.input_power = 1.0
        self.basic_properties.D_origin = 1.0
        self.structure = structure

    def unload_structure(self) -> None:
        """ Unloads any currently loaded sample structure. """
        self.structure = None

    def get_combined_properties(self,
                                px_size_nm: float) -> ExplicitSampleProperties:
        """
        Returns explicit sample properties. Properties derived from any loaded sample structure will
        take precedence over properties defined in basic_properties.
        """
        if self.structure is not None:
            return self.structure.get_explicit_properties(px_size_nm)
        elif self.basic_properties is not None:
            return self.basic_properties
        else:
            raise Exception("basic_properties or structure must be set!")
Пример #8
0
class Pulse:
    """
    A description of a laser pulse.
    """

    wavelength: Union[float, Multivalue[float]] = extended_field(
        observable_property("_wavelength", default=0.0, signal_name="basic_field_changed"),
        description="wavelength [nm]", accept_multivalues=True
    )

    duration: Union[float, Multivalue[float]] = extended_field(
        observable_property("_duration", default=0.0, signal_name="basic_field_changed"),
        description="duration [ms]", accept_multivalues=True
    )

    max_intensity: Union[float, Multivalue[float]] = extended_field(
        observable_property("_max_intensity", default=0.0, signal_name="basic_field_changed"),
        description="max intensity [kW/cm²]", accept_multivalues=True
    )

    illumination_pattern: Pattern = extended_field(default_factory=Pattern,
                                                   description="illumination pattern")
Пример #9
0
class AiryNAPatternData(RadialPatternData):
    """
    A description of the properties of an airy pattern.
    """

    na: Union[float, Multivalue[float]] = extended_field(1.4, description="NA")
    emission_wavelength: Union[float, Multivalue[float]] = extended_field(
        500.0, description="em. wavelength [nm]")

    # Methods
    def get_numpy_array(self,
                        canvas_inner_radius_nm: float,
                        px_size_nm: float,
                        extend_sides_to_diagonal: bool = False) -> np.ndarray:
        return generate_airy(amplitude=1.0,
                             fwhm=self.emission_wavelength / (2 * self.na),
                             canvas_radius=get_canvas_radius_nm(
                                 canvas_inner_radius_nm,
                                 extend_sides_to_diagonal),
                             px_size_nm=px_size_nm)

    def __str__(self) -> str:
        return f"Airy; NA = {self.na}, emission wavelength = {self.emission_wavelength} nm"
Пример #10
0
class PhysicalPinholePatternData(RadialPatternData):
    """
    A description of the properties of a physical pinhole pattern.
    """

    radius: Union[float, Multivalue[float]] = extended_field(
        100.0, description="radius [nm]")

    # Methods
    def get_numpy_array(self,
                        canvas_inner_radius_nm: float,
                        px_size_nm: float,
                        extend_sides_to_diagonal: bool = False) -> np.ndarray:
        return generate_physical_pinhole(radius=self.radius,
                                         canvas_radius=get_canvas_radius_nm(
                                             canvas_inner_radius_nm,
                                             extend_sides_to_diagonal),
                                         px_size_nm=px_size_nm)

    def __str__(self) -> str:
        return f"Physical pinhole; radius = {self.radius} nm"
Пример #11
0
class FluorophoreSettings:
    """
    A description of a fluorophore.
    """

    responses: List[IlluminationResponse] = extended_field(
        dataclass_property(
            fget=lambda self: self._get_responses(),
            fset=lambda self, responses: self._set_responses(responses),
            default=list),
        description=lambda self, index: str(self.responses[index]))

    # Methods
    def add_response(self, response: IlluminationResponse) -> bool:
        """
        Adds a response. Returns true if successful, or false if the wavelength was invalid or
        there is an existing response that includes the wavelength.
        """

        for existing_response in self._responses.values():
            if existing_response.wavelength == response.wavelength:
                return False

        self._responses[response.wavelength] = response
        self.response_added.emit(response)
        return True

    def remove_response(self, wavelength: float) -> None:
        """ Removes the response with the specified wavelength attributes. """
        removed_response = self._responses.pop(wavelength)
        self.response_removed.emit(removed_response)

    def clear_responses(self) -> None:
        """ Removes all responses. """
        for wavelength in [*self._responses.keys()]:
            self.remove_response(wavelength)

    def get_response(self,
                     wavelength: float) -> Optional[IlluminationResponse]:
        """
        Returns the response with the specified wavelength, if it is set. If not, a response
        obtained from interpolation is returned if there is more than one response, the only
        response if there is exactly one, or None if no responses are set.
        """

        # Look for exact match
        for response in self._responses.values():
            if wavelength == response.wavelength:
                return response

        # Interpolate if there's more than one response
        if len(self._responses.values()) > 1:
            interp_array_x = np.zeros(len(self._responses))
            interp_array_y = np.zeros((len(interp_array_x), 3))

            for index, response in enumerate(self._responses.values()):
                interp_array_x[index] = response.wavelength
                interp_array_y[index] = [
                    response.cross_section_off_to_on,
                    response.cross_section_on_to_off,
                    response.cross_section_emission
                ]

            interp_function = interp1d(interp_array_x,
                                       interp_array_y,
                                       axis=0,
                                       kind=max(
                                           1, min(len(interp_array_x) - 1, 3)),
                                       fill_value="extrapolate")

            return IlluminationResponse(wavelength,
                                        *interp_function(wavelength).clip(0))

        # Return only response if one exists
        if len(self._responses.values()) == 1:
            return list(self._responses.values())[0]

        # No responses, return None
        return None

    # Internal methods
    def _get_responses(self) -> List[IlluminationResponse]:
        return [*self._responses.values()]

    def _set_responses(self, responses: List[IlluminationResponse]) -> None:
        if not isinstance(responses, list):
            return

        self.clear_responses()
        for response in responses:
            self.add_response(response)
class Pattern:
    """
    A description of a pattern.
    """

    pattern_type: PatternType = PatternType.array2D
    pattern_data: Union[dict, PatternData] = extended_field(default_factory=Array2DPatternData,
                                                            description="pattern data")

    # Properties
    @property
    def pattern_data(self) -> PatternData:
        # The way we do this with allowing and converting from dicts is a bit stupid, but it seems
        # to be necessary in order to get dataclasses-json to encode/decode our data as wanted
        if isinstance(self._pattern_data, dict):
            self._pattern_data = self._get_data_type_from_pattern_type(self.pattern_type).from_dict(
                self._pattern_data
            )

        return self._pattern_data

    @pattern_data.setter
    def pattern_data(self, pattern_data: Union[dict, PatternData]) -> None:
        self._pattern_data = pattern_data
        self.data_loaded.emit(self)

    # Methods
    def __init__(self, pattern_type: Optional[PatternType] = None,
                 pattern_data: Optional[Union[dict, PatternData]] = None):
        if pattern_type is None and pattern_data is None:
            self.pattern_type = PatternType.array2D
            self.pattern_data = Array2DPatternData()
        elif pattern_data is not None:
            if isinstance(pattern_data, dict):
                if pattern_type is None:
                    raise ValueError(
                        "Pattern type must be specified when loading pattern data from dict"
                    )

                self.pattern_type = pattern_type
                self.pattern_data = pattern_data
            else:
                self.load_from_data(pattern_data)
        else:
            self.load_from_type(pattern_type)
    
    def load_from_type(self, pattern_type: PatternType) -> None:
        """
        Loads the given pattern type into pattern_type and a default pattern of that type into
        pattern_data.
        """
        self.pattern_type = pattern_type
        self.pattern_data = self._get_data_type_from_pattern_type(pattern_type)()

    def load_from_data(self, pattern_data: PatternData) -> None:
        """
        Loads the given pattern data into pattern_data and the type of that data into pattern_type.
        """
        self.pattern_type = self._get_pattern_type_from_data(pattern_data)
        self.pattern_data = pattern_data

    def get_radial_profile(self, canvas_inner_radius_nm: float, px_size_nm: float) -> np.ndarray:
        """ Returns a numpy array representation of the pattern data as a radial profile. """
        return self.pattern_data.get_radial_profile(canvas_inner_radius_nm, px_size_nm)

    def get_numpy_array(self, canvas_inner_radius_nm: float, px_size_nm: float,
                        extend_sides_to_diagonal: bool = False) -> np.ndarray:
        """ Returns a numpy array representation of the pattern data. """
        return self.pattern_data.get_numpy_array(canvas_inner_radius_nm, px_size_nm,
                                                 extend_sides_to_diagonal)

    def __str__(self) -> str:
        return str(self.pattern_data)

    # Internal methods
    def _get_data_type_from_pattern_type(self, pattern_type: PatternType) -> type:
        if pattern_type == PatternType.array2D:
            return Array2DPatternData
        elif pattern_type == PatternType.gaussian:
            return GaussianPatternData
        elif pattern_type == PatternType.doughnut:
            return DoughnutPatternData
        elif pattern_type == PatternType.airy_from_FWHM:
            return AiryFWHMPatternData
        elif pattern_type == PatternType.airy_from_NA:
            return AiryNAPatternData
        elif pattern_type == PatternType.digital_pinhole:
            return DigitalPinholePatternData
        elif pattern_type == PatternType.physical_pinhole:
            return PhysicalPinholePatternData
        else:
            raise ValueError(f"Invalid pattern type \"{pattern_type}\"")

    def _get_pattern_type_from_data(self, pattern_data: PatternData) -> PatternType:
        if type(pattern_data) is Array2DPatternData:
            return PatternType.array2D
        elif type(pattern_data) is GaussianPatternData:
            return PatternType.gaussian
        elif type(pattern_data) is DoughnutPatternData:
            return PatternType.doughnut
        elif type(pattern_data) is AiryFWHMPatternData:
            return PatternType.airy_from_FWHM
        elif type(pattern_data) is AiryNAPatternData:
            return PatternType.airy_from_NA
        elif type(pattern_data) is DigitalPinholePatternData:
            return PatternType.digital_pinhole
        elif type(pattern_data) is PhysicalPinholePatternData:
            return PatternType.physical_pinhole
        else:
            raise TypeError(f"Invalid pattern data type \"{type(pattern_data).__name__}\"")
Пример #13
0
class RunInstance:
    """
    A description of all parameters part of a simulation that can be run.
    """

    fluorophore_settings: FluorophoreSettings = extended_field(
        observable_property("_fluorophore_settings",
                            default=FluorophoreSettings,
                            signal_name="fluorophore_settings_loaded",
                            emit_arg_name="fluorophore_settings"),
        description="fluorophore settings")

    imaging_system_settings: ImagingSystemSettings = extended_field(
        observable_property("_imaging_system_settings",
                            default=ImagingSystemSettings,
                            signal_name="imaging_system_settings_loaded",
                            emit_arg_name="imaging_system_settings"),
        description="imaging system settings")

    pulse_scheme: PulseScheme = extended_field(observable_property(
        "_pulse_scheme",
        default=PulseScheme,
        signal_name="pulse_scheme_loaded",
        emit_arg_name="pulse_scheme"),
                                               description="pulse scheme")

    sample_properties: SampleProperties = extended_field(
        observable_property("_sample_properties",
                            default=SampleProperties,
                            signal_name="sample_properties_loaded",
                            emit_arg_name="sample_properties"),
        description="sample properties")

    detector_properties: DetectorProperties = extended_field(
        observable_property("_detector_properties",
                            default=DetectorProperties,
                            signal_name="detector_properties_loaded",
                            emit_arg_name="detector_properties"),
        description="detector properties")

    simulation_settings: SimulationSettings = extended_field(
        observable_property("_simulation_settings",
                            default=SimulationSettings,
                            signal_name="simulation_settings_loaded",
                            emit_arg_name="simulation_settings"),
        description="simulation settings")

    # Methods
    def simulate(self,
                 *,
                 cache_kernels2d: bool = True,
                 precache_frc_curves: bool = True,
                 preprocessing_finished_callback: Optional[Signal] = None,
                 progress_updated_callback: Optional[Signal] = None):
        """ Runs the simulation and returns the results. """
        return simulate(
            deepcopy(self),
            cache_kernels2d=cache_kernels2d,
            precache_frc_curves=precache_frc_curves,
            abort_signal=self._abort_signal,
            preprocessing_finished_callback=preprocessing_finished_callback,
            progress_updated_callback=progress_updated_callback)

    def abort_running_simulations(self) -> None:
        """
        Emits a signal to abort any running simulations. Note that the simulations may not terminate
        immediately after this method returns.
        """
        self._abort_signal.emit()
Пример #14
0
class PulseScheme:
    """
    A description of a laser pulse scheme.
    """

    pulses: List[Pulse] = extended_field(
        dataclass_property(
            fget=lambda self: self._get_pulses(),
            fset=lambda self, pulses: self._set_pulses(pulses),
            default=list
        ),
        description=lambda _self, index: f"Pulse #{index + 1}"
    )

    # Methods
    def add_pulse(self, pulse: Pulse) -> None:
        """ Adds a pulse to the pulse scheme. """
        key = str(uuid.uuid4())  # Generate random key
        self._pulses[key] = pulse
        self.pulse_added.emit(key, pulse)

    def remove_pulse(self, key: str) -> None:
        """ Removes the pulse with the specified key from the pulse scheme. """
        removed_pulse = self._pulses.pop(key)
        self.pulse_removed.emit(key, removed_pulse)

    def clear_pulses(self) -> None:
        """ Removes all pulses from the pulse scheme. """
        keys = [*self._pulses.keys()]
        for key in keys:
            self.remove_pulse(key)

    def move_pulse_left(self, key: str) -> None:
        """ Moves the pulse with the specified key one step to the left in the order. """
        existing_keys = [*self._pulses.keys()].copy()

        i = len(existing_keys) - 1
        while i >= 0:
            has_moved_requested = False
            if key == existing_keys[i] and 0 < i:
                self._pulses.move_to_end(existing_keys[i - 1], last=False)
                has_moved_requested = True

            self._pulses.move_to_end(existing_keys[i], last=False)
            if has_moved_requested:
                i -= 2
            else:
                i -= 1

        self.pulse_moved.emit(key, self._pulses[key])

    def move_pulse_right(self, key: str) -> None:
        """ Moves the pulse with the specified key one step to the right in the order. """
        existing_keys = [*self._pulses.keys()].copy()

        i = 0
        while i < len(existing_keys):
            has_moved_requested = False
            if key == existing_keys[i] and len(existing_keys) > i + 1:
                self._pulses.move_to_end(existing_keys[i + 1], last=True)
                has_moved_requested = True

            self._pulses.move_to_end(existing_keys[i], last=True)
            if has_moved_requested:
                i += 2
            else:
                i += 1

        self.pulse_moved.emit(key, self._pulses[key])

    def get_pulses_with_keys(self):
        return self._pulses.items()

    # Internal methods
    def _get_pulses(self) -> List[Pulse]:
        return [*self._pulses.values()]

    def _set_pulses(self, pulses: List[Pulse]) -> None:
        if not isinstance(pulses, list):
            return

        self.clear_pulses()
        for pulse in pulses:
            self.add_pulse(pulse)