Source code for finesse.detectors.general

"""Top-level objects which specific detectors should inherit from."""

from abc import ABC

import numpy as np

from ..element import ModelElement
from ..components import Node
from ..exceptions import ContextualTypeError
from ..utilities.homs import make_modes, insert_modes, remove_modes
from ..env import warn


[docs]class Detector(ABC, ModelElement): """Abstract representation of a component that produces a numerical output. User detector classes should subclass this class. The simulation will then generate a dictionary of output values. Parameters ---------- name : str Name of newly created detector. node : :class:`.Node` or :class:`.Port` Node to read output from. If a port is given, it must have one node, so that is is unambiguous which node to use. dtype : type, optional The numpy datatype for which this output result will be stored in. unit : str, optional A human readable unit for the output. E.g. W, m, m/rtHz. """ def __init__( self, name, node=None, dtype=np.complex128, shape=(), unit="arb.", label=None, needs_fields=True, needs_trace=False, ): from finesse.detectors.workspace import OutputInformation ModelElement.__init__(self, name) if not isinstance(node, (Node, type(None))): try: # try and grab a node if len(node.nodes) > 1: # node selection not obvious, inform user about assumption warn( "Detector node not specified. Assuming " f"'{node.component.name}.{node.name}.{node.nodes[0].name}'." ) node = node.nodes[0] except Exception: raise ContextualTypeError("node", node, allowed_types=(Node,)) if node: node.used_in_detector_output.append(self) self.__output_information = OutputInformation( name, type(self), (node,), dtype, unit, shape, label, needs_fields, needs_trace, ) def _on_add(self, model): for node in self.__output_information.nodes: if node is not None and model is not node._model: raise Exception( f"{repr(self)} is using a node {node} from a different model" ) def _on_remove(self): if self.node: self.node.used_in_detector_output.remove(self) @property def output_information(self): return self.__output_information @property def needs_fields(self): """Flag indicating whether the detector requires light fields (i.e. solving of the interferometer matrix).""" return self.__output_information.needs_fields @property def needs_trace(self): """Flag indicating whether the detector requires beam traces.""" return self.__output_information.needs_trace @property def node(self): """The nodes this detector observes. :getter: Returns the detected node. """ return self.__output_information.nodes[0] @property def dtype(self): return self.__output_information.dtype @property def dtype_shape(self): return self.__output_information.dtype_shape def _update_dtype_shape(self, shape): """Only to be used internally by detectors like Cameras for updating the shape if resolution is changed.""" self.__output_information._update_dtype_shape(shape) def _update_dtype(self, dtype): """Only to be used internally by detectors like MathDetectors for updating the dtype if the expression is changed.""" self.__output_information._set_dtype(dtype) @property def dtype_size(self): """Size of the output in terms of number of elements. This is typically unity as most detectors return a single value via their output functions. Equivalent to the product of :attr:`.Detector.dtype_shape`. """ return int(np.prod(self.__output_information.dtype_shape)) @property def unit(self): return self.__output_information.unit def _update_unit(self, unit): self.__output_information._update_unit(unit) @property def label(self): return self.__output_information.label def _update_label(self, label): self.__output_information._update_label(label)
[docs]class MaskedDetector(Detector, ABC): """An abstract class from which detector types which can have mode masks derive. Any detector object which calculates quantities involving loops over the modes of a model should inherit from this --- allowing masks to be applied to mode patterns via the methods of this class. Examples of detectors which should derive from `MaskedDetector` are power-detectors, amplitude-detectors and cameras. """
[docs] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # The mode indices which will be masked # on this detector in simulations self._mask = np.zeros(0, dtype=(np.intc, 2))
@property def has_mask(self): """Whether the detector has a mask applied to it. Simply checks to see if the :attr:`.MaskedDetector.mask` length is non-zero. """ return len(self.mask) @property def mask(self): """An array of HOMs to mask from the output. Any contributions from these modes will be zeroed when calculating the detector output. :getter: Returns the array of masked indices. :setter: Sets the masked indices. See :meth:`.MaskedDetector.select_mask` for the options available. """ return self._mask @mask.setter def mask(self, value): self.select_mask(value)
[docs] def select_mask(self, modes=None, maxtem=None, exclude=None): """Select the HOM indices to include in the mask. The mode-selecting examples in :ref:`selecting_modes` may be referred to for typical patterns when using this method, as the same concepts apply equally to making detector masks (equivalent code under-the-hood). Parameters ---------- modes : sequence, str, optional; default: None Identifier for the mode indices to generate. This can be: - An iterable of mode indices, where each element in the iterable must unpack to two integer convertible values. - A string identifying the type of modes to include, must be one of "even", "odd", "tangential" (or "x") or "sagittal" (or "y"). By default this is None, such that, for example, this method can be used to select a mask of all modes up to a given `maxtem`. maxtem : int, optional; default: None Optional maximum mode order. If not specified then the maxtem used internally will be equal to the maximum mode order of the associated model. Note that this argument is ignored if `modes` is an iterable of mode indices. exclude : sequence, str, optional; default: None A mode, or iterable of modes, to exclude from the selected pattern. For example, if one calls ``select_mask("even", exclude="00")`` then the mask will be an array of all even-order HOM indices excluding the 00 mode. Examples -------- See :ref:`selecting_modes`. """ if maxtem is None: if self.has_model: maxtem = self._model.modes_setting["maxtem"] self._mask = make_modes(modes, maxtem) if exclude is not None: self.remove_from_mask(exclude)
[docs] def add_to_mask(self, modes): """Inserts the specified mode indices into the detector mask. Parameters ---------- modes : sequence, str A single mode index pair or an iterable of mode indices. Each element must unpack to two integer convertible values. """ self._mask = insert_modes(self._mask, modes)
[docs] def remove_from_mask(self, modes): """Removes the specified mode indices from the detector mask. Parameters ---------- modes : sequence, str A single mode index pair or an iterable of mode indices. Each element must unpack to two integer convertible values. """ self._mask = remove_modes(self._mask, modes)
[docs]class SymbolDetector(Detector):
[docs] def __init__(self, name, symbol, dtype=np.complex128): Detector.__init__(self, name, None, dtype=dtype) self.symbol = symbol
[docs] def get_output(self, *args): return self.dtype(self.symbol.eval())
[docs]class NoiseDetector: def __init__(self, noise_type): self.__noise_type = noise_type self._requested_selection_vectors = dict() self.__selection_vectors = {} @property def noise_type(self): return self.__noise_type def _request_selection_vector(self, name): self._requested_selection_vectors[name] = -1