lens.py 6.24 KB
Newer Older
1 2 3 4
"""
Transmissive optical components which focus or disperse light beams.
"""

5
import logging
6

7
import numpy as np
8

9
from finesse.components.modal.lens import (
10
    LensWorkspace
11
)
12
from finesse.components.general import Connector, InteractionType
13
from finesse.components.node import NodeDirection, NodeType
14
from finesse.parameter import model_parameter, Rebuild
15
from finesse.utilities import refractive_index
16

17 18
LOGGER = logging.getLogger(__name__)

19

20
@model_parameter(
21 22 23 24 25 26 27
    "f",
    "Focal length",
    np.inf,
    Rebuild.HOM,
    validate="_check_f",
    units="m",
    is_geometric=True,
28
)
29
class Lens(Connector):
30 31
    """Represents a thin lens optical component with an associated focal length.

32 33 34 35
    Notes
    -----
    The specified focal length `f` is only accurate when the lens is attached to spaces with index
    of refraction close to 1. This component exists so that one can use the intuitive focal length
36
    parameter instead of having to set radii of curvature as with e.g. :class:`.Mirror`.
37

38 39 40 41 42 43
    Parameters
    ----------
    name : str
        Name of newly created lens.

    f : float, optional
44
        Focal length of the lens in metres; defaults to infinity.
45
    """
Samuel Rowlinson's avatar
Samuel Rowlinson committed
46

47
    def __init__(self, name, f=np.inf):
48
        super().__init__(name)
49 50 51

        self.f = f

52
        self._add_port("p1", NodeType.OPTICAL)
Samuel Rowlinson's avatar
Samuel Rowlinson committed
53 54
        self.p1._add_node("i", NodeDirection.INPUT)
        self.p1._add_node("o", NodeDirection.OUTPUT)
55

56
        self._add_port("p2", NodeType.OPTICAL)
Samuel Rowlinson's avatar
Samuel Rowlinson committed
57 58
        self.p2._add_node("i", NodeDirection.INPUT)
        self.p2._add_node("o", NodeDirection.OUTPUT)
59

60
        # optic to optic couplings
Samuel Rowlinson's avatar
Samuel Rowlinson committed
61
        self._register_node_coupling(
62 63 64 65
            "P1i_P2o",
            self.p1.i,
            self.p2.o,
            interaction_type=InteractionType.TRANSMISSION,
Samuel Rowlinson's avatar
Samuel Rowlinson committed
66 67
        )
        self._register_node_coupling(
68
            "P2i_P1o",
69 70 71
            self.p2.i,
            self.p1.o,
            interaction_type=InteractionType.TRANSMISSION,
Samuel Rowlinson's avatar
Samuel Rowlinson committed
72
        )
73

74
    def _check_f(self, value):
75
        if value == 0:
76
            raise ValueError("Focal length of lens must be non-zero.")
77

78 79
        return value

Daniel Brown's avatar
Daniel Brown committed
80 81 82 83
    def _resymbolise_ABCDs(self):
        self.__symbolise_ABCDs("x")
        self.__symbolise_ABCDs("y")

84 85 86
    def __symbolise_ABCDs(self, direction):
        # TODO (sjr) Not using direction currently but will split
        #            into fx, fy soon to support astigmatic lenses
87 88
        M_sym = np.array([[1.0, 0.0], [-1.0 / self.f.ref, 1.0]])
        M_num = np.array(M_sym, dtype=np.float64)
89
        key = (self.p1.i, self.p2.o, direction)
90
        self._abcd_matrices[key] = (M_sym, M_num)
91
        key = (self.p2.i, self.p1.o, direction)
92
        self._abcd_matrices[key] = (M_sym, M_num)
93

94 95 96 97 98 99 100 101
    @property
    def abcdx(self):
        """Numeric ABCD matrix in the tangential plane.

        Equivalent to ``lens.ABCD(1, 2, "x")`` and ``lens.ABCD(2, 1, "x")``.

        :getter: Returns a copy of the (numeric) ABCD matrix for this coupling (read-only).
        """
102
        _, M = self._abcd_matrices[(self.p1.i, self.p2.o, "x")]
103 104 105 106 107 108 109 110 111 112
        return M.copy()

    @property
    def abcdy(self):
        """Numeric ABCD matrix in the sagittal plane.

        Equivalent to ``lens.ABCD(2, 1, "y")`` and ``lens.ABCD(2, 1, "y")``.

        :getter: Returns a copy of the (numeric) ABCD matrix for this coupling (read-only).
        """
113
        _, M = self._abcd_matrices[(self.p1.i, self.p2.o, "y")]
114 115
        return M.copy()

116
    def ABCD(
117 118 119 120 121 122 123
        self,
        from_node,
        to_node,
        direction="x",
        symbolic=False,
        copy=True,
        retboth=False,
124
    ):
125
        r"""Returns the ABCD matrix of the lens for the specified coupling.
126 127 128 129 130 131

        .. _fig_abcd_lens_transmission:
        .. figure:: ../images/abcd_lenst.*
            :align: center

        This is given by,
132 133 134 135 136 137 138 139 140

        .. math::
            M = \begin{pmatrix}
                    1 & 0 \\
                    -\frac{1}{f} & 1
                \end{pmatrix},

        where :math:`f` is the focal length of the lens.

141 142
        See :meth:`.Connector.ABCD` for descriptions of parameters, return values and possible
        exceptions.
143
        """
144
        return super().ABCD(from_node, to_node, direction, symbolic, copy, retboth)
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
    def actuate_f(self, dioptres, direction=("x", "y")):
        r"""Actuate on the focal length of the lens with a
        specified dioptre shift.

        Sets the focal length to a new value, :math:`f_2`, via,

        .. math::

            f_2 = \frac{1}{d + \frac{1}{f_1}},

        where :math:`f_1` is the current focal length and :math:`d` is the dioptre shift (i.e.
        the `dioptre` argument).

        By default, both focal planes are shifted. To shift, e.g., only the
        tangential plane, specify ``direction="x"``.

        Parameters
        ----------
        dioptres : float
            Shift in lens focal length in dioptres.

        direction : tuple or str, optional; default: ("x", "y")
            Focal plane to shift, defaults to both tangential and sagittal.
        """
        fnew = lambda x: 1 / (dioptres + 1 / x)
        if "x" in direction:  # TODO (sjr) Change to fx
            self.f = fnew(self.f.value)
        if "y" in direction:  # TODO (sjr) Change to fy
            self.f = fnew(self.f.value)

176 177 178
    def _fill_matrix(self, ws):
        for freq in ws.sim.frequencies:
            with ws.sim.component_edge_fill3(
179
                ws.owner_id, ws.connections.P1i_P2o_idx, freq.index, freq.index,
180
            ) as mat:
181
                mat[:] = ws.K12.data
182 183

            with ws.sim.component_edge_fill3(
184
                ws.owner_id, ws.connections.P2i_P1o_idx, freq.index, freq.index,
185
            ) as mat:
186 187
                mat[:] = ws.K21.data

188 189
    def _get_workspace(self, sim):
        _, is_changing = self._eval_parameters()
190
        refill = self in sim.trace_forest.changing_components or len(is_changing)
191 192

        ws = LensWorkspace(self, sim, refill)
193 194 195 196
        # This assumes that nr1/nr2 cannot change during a simulation
        ws.nr1 = refractive_index(self.p1) or 1
        ws.nr2 = refractive_index(self.p2) or 1

197 198
        ws.set_fill_fn(self._fill_matrix)

199
        if sim.is_modal:
Daniel Brown's avatar
Daniel Brown committed
200 201 202 203
            key = (self.p1.i, self.p2.o, "x")
            _, ws.abcd_x = self._abcd_matrices[key]
            key = (self.p1.i, self.p2.o, "y")
            _, ws.abcd_y = self._abcd_matrices[key]
204

205
        return ws