Source code for finesse.components.surface

from abc import ABC
from math import isclose
import numpy as np

from ..parameter import float_parameter
from ..symbols import Resolving, Symbol
from ..utilities.misc import calltracker
from .general import Connector
from finesse.exceptions import InvalidRTLError, EvaluateResolvingSymbolError
from finesse.utilities import clip_with_tolerance


[docs]@float_parameter("R", "Reflectivity", validate="_check_R", post_validate="check_rtl") @float_parameter("T", "Transmission", validate="_check_T", post_validate="check_rtl") @float_parameter("L", "Loss", validate="_check_L", post_validate="check_rtl") @float_parameter("phi", "Phase", units="degrees") @float_parameter( "Rcx", "Radius of curvature (x)", units="m", validate="_check_Rc", is_geometric=True, ) @float_parameter( "Rcy", "Radius of curvature (y)", units="m", validate="_check_Rc", is_geometric=True, ) @float_parameter("xbeta", "Misalignment (x)", units="radians") @float_parameter("ybeta", "Misalignment (y)", units="radians") # IMPORTANT: renaming this class impacts the katscript spec and should be avoided! class Surface(ABC, Connector): """Abstract optical surface interface providing an object with common properties for :class:`.Mirror` and :class:`.Beamsplitter` to inherit from. Parameters ---------- name : str Name of newly created surface. R : float, optional Reflectivity of the surface. T : float, optional Transmissivity of the surface. L : float, optional Loss of the surface. phi : float, optional Microscopic tuning of the surface (in degrees). Rc : float, optional Radius of curvature (in metres); defaults to ``numpy.inf`` to indicate a planar surface. An astigmatic surface can be set with `Rc = (Rcx, Rcy)`. """
[docs] def __init__(self, name, R, T, L, phi, Rc, xbeta, ybeta): Connector.__init__(self, name) # must be set before set_RTL is called self._check_rtl = True # only in constructor allow setting of none of R, T, L # -> default to equally reflective and tranmissive with no loss if R is None and T is None and L is None: # Use some default self.set_RTL(0.5, T=0.5, L=0) else: self.set_RTL(R, T, L) self.phi = phi self.Rc = Rc self.xbeta = xbeta self.ybeta = ybeta
def _check_R(self, value): value = clip_with_tolerance(value, 0, 1) if not 0 <= value <= 1: raise InvalidRTLError( f"Reflectivity must satisfy 0 <= R <= 1, but is {value}" ) return value def _check_T(self, value): value = clip_with_tolerance(value, 0, 1) if not 0 <= value <= 1: raise InvalidRTLError( f"Transmissivity must satisfy 0 <= T <= 1, but is {value}" ) return value def _check_L(self, value): value = clip_with_tolerance(value, 0, 1) if not 0 <= value <= 1: raise InvalidRTLError(f"Loss must satisfy 0 <= L <= 1, but is {value}") return value def _check_Rc(self, value): if value == 0: raise InvalidRTLError("Radius of curvature must be non-zero.") return value
[docs] def set_RTL(self, R=None, T=None, L=None): """Set the values for the R, T and L properties of the surface. One of the following combination must be specified: - R and T or, - R and L or, - T and L or, - R and T and L In the first three cases, the remaining parameter is set via the condition, .. math:: R + T + L = 1 Parameters ---------- R : scalar Value of the reflectivity to set. T : scalar Value of the transmissivity to set. L : scalar Value of the loss to set. Raises ------ ValueError If a combination other than one of the above is specified. Or if R, T and L are all given but they sum to anything other than one. """ self._check_rtl = False try: return self._set_RTL(R=R, T=T, L=L) finally: self._check_rtl = True self.check_rtl()
@calltracker def _set_RTL(self, R=None, T=None, L=None): # Try and cast the input into a float, the datatype of the # R/T/L parameters. If it can't just ignore it, as it could be # None or some callable, or something else. Need this because # the usual datatype casting doesn't happen until much later # with this setter, and it is used to R/T/L in the contructor # not self.R = R, etc. if R is not None and not isinstance(R, Symbol): R = self.R.datatype_cast(R) if T is not None and not isinstance(T, Symbol): T = self.T.datatype_cast(T) if L is not None and not isinstance(L, Symbol): L = self.L.datatype_cast(L) # Count number of specified parameters in R, T, L N = sum(x is not None for x in (R, T, L)) if N < 2: msg = f"""Invalid combination passed to {self.name}.set_RTL. One of the following must be specified: - R and T or, - R and L or, - T and L or, - R and T and L """ raise InvalidRTLError(msg.strip()) if N == 2: old_R = self.R.value old_T = self.T.value old_L = self.L.value # We have two out of the three: check which one is None try: if L is None: self.R = R self.T = T self.L = 1 - (self.R.ref + self.T.ref) elif T is None: self.R = R self.L = L self.T = 1 - (self.R.ref + self.L.ref) elif R is None: self.T = T self.L = L self.R = 1 - (self.T.ref + self.L.ref) else: raise InvalidRTLError(f"Unexpected combination R={R}, T={T}, L={L}") except InvalidRTLError: self.R = old_R self.T = old_T self.L = old_L raise else: # i.e. N==3 # TODO should be context manager self.R = R self.T = T self.L = L
[docs] def check_rtl(self): if not self._check_rtl: return R = self.R.value T = self.T.value L = self.L.value if R is None or T is None or L is None: return for par, name in zip((R, T, L), ("R", "T", "L")): if isinstance(par, Symbol) and not isinstance(par, Resolving): name = f"{self.name}.{name}" try: val = clip_with_tolerance(par.eval(), 0, 1) if not 0 <= val <= 1: raise InvalidRTLError( f"Symbolic parameter {name} must satisfy 0 <= {name} <= 1, " f"but {name}={par}={val}" ) except EvaluateResolvingSymbolError: # check_rtl should be called again after symbols are resolved pass RTL_sum = R + T + L if any(isinstance(par, Resolving) for par in (R, T, L)): return if isinstance(RTL_sum, Symbol): # Evaluate symbolics, if necessary. try: RTL_sum = RTL_sum.eval() except EvaluateResolvingSymbolError: # check_rtl should be called again after symbols are resolved return # FIXME: decide what the necessary tolerance is here. if not isclose(RTL_sum, 1): msg = ( f"Expected R + T + L = 1 in '{self.name}' but " f"got R + T + L = {RTL_sum}" ) raise InvalidRTLError(msg)
@property def Rc(self): """The radius of curvature of the mirror in metres, for both the tangential and sagittal planes. :`getter`: Returns values of both planes' radii of curvature as a :class:`numpy.ndarray` where the first element is the tangential plane RoC and the second element is the sagittal plane RoC. :`setter`: Sets the radius of curvature. Examples -------- The following sets the radii of curvature of an object `m`, which is a sub-instance of `Surface`, in both directions to 2.5 m: >>> m.Rc = 2.5 Whilst this would set the radius of curvature in the x-direction (tangential plane) to 2.5 m and the radius of curvature in the y-direction (sagittal plane) to 2.7 m: >>> m.Rc = (2.5, 2.7) """ return np.array([self.Rcx.value, self.Rcy.value]) @Rc.setter def Rc(self, value): try: self.Rcx = value[0] self.Rcy = value[1] except (IndexError, TypeError): self.Rcx = value self.Rcy = value
[docs] def actuate_roc(self, dioptres, direction=("x", "y")): r"""Actuate on the radius of curvature (RoC) of the surface with a specified dioptre shift. Sets the RoC to a new value, :math:`R_2`, via, .. math:: R_2 = \frac{2}{d + \frac{2}{R_1}}, where :math:`R_1` is the current RoC and :math:`d` is the dioptre shift (i.e. the `dioptre` argument). By default, both planes of curvature are shifted. To shift, e.g., only the tangential plane, specify ``direction="x"``. Parameters ---------- dioptres : float Shift in surface RoC in dioptres. direction : tuple or str, optional; default: ("x", "y") RoC plane to shift, defaults to both tangential and sagittal. """ rcnew = lambda x: 2 / (dioptres + 2 / x) if "x" in direction: self.Rcx = rcnew(self.Rcx.value) if "y" in direction: self.Rcy = rcnew(self.Rcy.value)
# NOTE this is a bit hacky but gets around using surface.R = value (etc.) # directly in an axis scan without being warned def _on_add(self, model): super()._on_add(model) # we alse need to call check_rtl after parsing is completed and self-referencing # symbols are resolved as well. model._on_parse.append(self.check_rtl)