Source code for finesse.script.spec

"""Kat script specification.

This defines supported kat script syntax and maps it to Finesse Python classes via
adapters.
"""

import abc
from difflib import get_close_matches
import logging
from collections import ChainMap
from .. import components, detectors, locks, symbols
from ..model import Model
from ..components import mechanical, electronics
from ..components.ligo import suspensions as ligo
from ..analysis import actions
from ..analysis import noise
from .adapter import ElementAdapter, CommandAdapter, AnalysisAdapter, GetterProxy


LOGGER = logging.getLogger(__name__)


class _GuassGetterProxy(GetterProxy):
    def __call__(self, gauss):
        # Use the :attr:`.Gauss._specified_params` attribute to dump the `kwargs`
        # signature argument.
        return [], {"name": gauss.name, "node": gauss.node, **gauss._specified_params}


class _FsigGetterProxy(GetterProxy):
    def __call__(self, model):
        if model.fsig.f.value is None:
            return

        return [model.fsig.f], {}


def _set_fsig(model, f):
    """Signal input frequency.

    Parameters
    ----------
    f : float or :class:`.Symbol`
        The frequency.
    """
    model.fsig.f = f


class _LambdaGetterProxy(GetterProxy):
    def __call__(self, model):
        return [model.lambda0], {}


def _set_lambda0(model, lambda0):
    """Reference wavelength.

    Parameters
    ----------
    lambda0 : float
        The reference wavelength.
    """
    model.lambda0 = lambda0


class _ModesGetterProxy(GetterProxy):
    def __call__(self, model):
        modes = model.modes_setting

        # Filter out empty values.
        modes = {key: value for key, value in modes.items() if value is not None}

        if not modes:
            return

        return [], modes


def _set_link(model, *args, **kwargs):
    model.link(*args, **kwargs)


class _IntrixGetterProxy(GetterProxy):
    def __call__(self, model):
        if not model.input_matrix_dc:
            return

        # FIXME: implement proper getter
        raise NotImplementedError("intrix dumping not yet implemented")


def _set_intrix(model, *args, **kwargs):
    """Set input matrix."""
    assert not kwargs

    DOF = args[0]
    factors, readouts = args[1::2], args[2::2]
    if len(factors) != len(readouts):
        raise Exception("must specify 'factor, readout' pairs")
    for factor, readout in zip(factors, readouts):
        model.input_matrix_dc[DOF, readout] = factor


class _TEMGetterProxy(GetterProxy):
    def __call__(self, model):
        """(args, kwargs) tuples for each defined TEM mode."""
        tems = []

        for laser in model.get_elements_of_type(components.Laser):
            for (n, m), (factor, phase) in laser.non_default_power_coeffs.items():
                tems.append(
                    ([laser], {"n": n, "m": m, "factor": factor, "phase": phase})
                )

        if not tems:
            # Don't generate anything.
            return

        return tems


def _set_tem(model, laser, *args, **kwargs):
    """Set laser TEM."""
    laser.tem(*args, **kwargs)


[docs]class BaseSpec(metaclass=abc.ABCMeta): """Empty language specification.""" _SUPPORTED_CONSTANTS = {} _SUPPORTED_KEYWORDS = set() _SUPPORTED_UNARY_OPERATORS = {} _SUPPORTED_BINARY_OPERATORS = {} _SUPPORTED_EXPRESSION_FUNCTIONS = {} _DEFAULT_ELEMENTS = [] _DEFAULT_COMMANDS = [] _DEFAULT_ANALYSES = []
[docs] def __init__(self): # Modifiable specifications. These are dynamically supported by the parser. self.elements = {} self.commands = {} self.analyses = {} # Fixed specifications. These are not modifiable by the user. self.constants = self._SUPPORTED_CONSTANTS self.keywords = self._SUPPORTED_KEYWORDS self.unary_operators = self._SUPPORTED_UNARY_OPERATORS self.binary_operators = self._SUPPORTED_BINARY_OPERATORS self.expression_functions = self._SUPPORTED_EXPRESSION_FUNCTIONS # Add support for the default directives. for elementdata in self._DEFAULT_ELEMENTS: self.register_element(*elementdata) for commanddata in self._DEFAULT_COMMANDS: self.register_command(*commanddata) for analysisdata in self._DEFAULT_ANALYSES: self.register_analysis(*analysisdata)
@property def directives(self): """All top level parser directives. :getter: Returns a mapping of top level parser directive aliases to :class:`adapters <.BaseAdapter>`. :type: :class:`~collections.ChainMap` """ # ChainMap yields in LIFO order so key order becomes elements, then commands, # then analyses. This order is relied upon by :func:`.syntax`. return ChainMap(self.analyses, self.commands, self.elements) @property def reserved_names(self): """All reserved names. This is primarily useful for tests. :getter: Returns the names reserved in the parser as special production types. :type: :class:`list` """ return list(self.keywords) + list(self.constants) def _register_adapter(self, ptype, mapping, aliases, kwargs=None, overwrite=False): if kwargs is None: kwargs = {} adapter = ptype(aliases, **kwargs) for alias in adapter.aliases: if alias in mapping: if overwrite: LOGGER.info(f"Overwriting existing '{alias}' with {adapter}") else: raise KeyError( f"'{alias}' from {adapter} already exists (provided by " f"{mapping[alias]}). If you intend to overwrite the existing " f"definition, set overwrite=True." ) mapping[alias] = adapter
[docs] def register_element(self, *args, **kwargs): """Add parser and generator support for a model element such as a component or detector. Other Parameters ---------------- aliases : str or sequence The element alias(es). kwargs : mapping, optional Keyword arguments to pass to the adapter constructor; defaults to None. overwrite : bool, optional Overwrite elements with the same aliases, if present. Defaults to False. """ self._register_adapter(ElementAdapter, self.elements, *args, **kwargs)
[docs] def register_command(self, *args, **kwargs): """Add parser and generator support for a command. Other Parameters ---------------- aliases : str or sequence The command alias(es). kwargs : mapping, optional Keyword arguments to pass to the adapter constructor; defaults to None. overwrite : bool, optional Overwrite commands with the same aliases, if present. Defaults to False. """ self._register_adapter(CommandAdapter, self.commands, *args, **kwargs)
[docs] def register_analysis(self, *args, **kwargs): """Add parser and generator support for an analysis. Other Parameters ---------------- aliases : str or sequence The analysis alias(es). kwargs : mapping, optional Keyword arguments to pass to the adapter constructor; defaults to None. overwrite : bool, optional Overwrite analyses with the same aliases, if present. Defaults to False. """ self._register_adapter(AnalysisAdapter, self.analyses, *args, **kwargs)
[docs] def adapter_by_setter(self, setter): """Get adapter given its Python setter. Parameters ---------- setter : type The setter to look up the adapter for. Returns ------- :class:`.BaseAdapter` The adapter corresponding to `setter`. Raises ------ ValueError If no adapter corresponding to `setter` could be found. """ for adapter in self.directives.values(): if adapter.setter == setter: return adapter raise ValueError(f"Could not find adapter for '{setter!r}'")
[docs] def match_fuzzy_directive(self, search, limit=3, cutoff=0.5): """Get the directives that most closely match the specified string. Parameters ---------- search : str The directive to search for. limit : int, optional The maximum number of matches to return. cutoff : float, optional The cutoff below which to assume no match. This is the ratio as defined in the `Python documentation <https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.ratio>`__. Returns ------- list Up to `limit` closest matches. """ return get_close_matches(search, self.directives, n=limit, cutoff=cutoff)
[docs]class KatSpec(BaseSpec): """Kat language specification. This defines the available instructions for the parser and the adapters that convert them to and from Python models. The default instructions, actions and keywords are built into public properties which may be modified by users (e.g. to add support for custom commands). As such, the internal defaults (fields with names beginning `_DEFAULT_`) should not be modified after import. """ # List of default elements in (aliases, type, kwargs) form. # :class:`.InstructionAdapter` objects are created for each element and the aliases # are each mapped to their corresponding adapter. Order here does not matter. _DEFAULT_ELEMENTS = [ # Components. ( ("amplifier", "amp"), {"setter": electronics.Amplifier, "getter": electronics.Amplifier}, ), ( ("beamsplitter", "bs"), {"setter": components.Beamsplitter, "getter": components.Beamsplitter}, ), # Cavity's `build_last` flag is set because it implicitly depends on any nodes # that appear in the path from its start port back to itself, so its # dependencies cannot be determined by the time the first set of elements are # built into the model. It is therefore moved to the second build pass by this # flag. ( ("cavity", "cav"), { "setter": components.Cavity, "getter": components.Cavity, "build_last": True, }, ), ( ("degree_of_freedom", "dof"), { "setter": components.DegreeOfFreedom, "getter": components.DegreeOfFreedom, }, ), ( ("directional_beamsplitter", "dbs"), { "setter": components.DirectionalBeamsplitter, "getter": components.DirectionalBeamsplitter, }, ), ( ("filter_zpk", "zpk"), {"setter": electronics.ZPKFilter, "getter": electronics.ZPKFilter}, ), ( ("filter_butter", "butter"), {"setter": electronics.ButterFilter, "getter": electronics.ButterFilter}, ), ( ("filter_cheby1", "cheby1"), {"setter": electronics.Cheby1Filter, "getter": electronics.Cheby1Filter}, ), ( ("isolator", "isol"), {"setter": components.Isolator, "getter": components.Isolator}, ), (("laser", "l"), {"setter": components.Laser, "getter": components.Laser}), ("lens", {"setter": components.Lens, "getter": components.Lens}), (("mirror", "m"), {"setter": components.Mirror, "getter": components.Mirror}), ( ("modulator", "mod"), {"setter": components.Modulator, "getter": components.Modulator}, ), ( ("optical_bandpass", "obp"), { "setter": components.optical_bandpass.OpticalBandpassFilter, "getter": components.optical_bandpass.OpticalBandpassFilter, }, ), ( ("squeezer", "sq"), {"setter": components.Squeezer, "getter": components.Squeezer}, ), ( "readout_dc", {"setter": components.ReadoutDC, "getter": components.ReadoutDC}, ), ( "readout_dc_qpd", {"setter": components.ReadoutDCQPD, "getter": components.ReadoutDCQPD}, ), ( "readout_rf", {"setter": components.ReadoutRF, "getter": components.ReadoutRF}, ), ( ("variable", "var"), {"setter": components.Variable, "getter": components.Variable}, ), # Detectors. ( ("amplitude_detector", "ad"), { "setter": detectors.AmplitudeDetector, "getter": detectors.AmplitudeDetector, }, ), ( "astigd", { "setter": detectors.AstigmatismDetector, "getter": detectors.AstigmatismDetector, }, ), ( ("beam_property_detector", "bp"), { "setter": detectors.BeamPropertyDetector, "getter": detectors.BeamPropertyDetector, }, ), ("ccd", {"setter": detectors.CCD, "getter": detectors.CCD}), ("ccdline", {"setter": detectors.CCDScanLine, "getter": detectors.CCDScanLine}), ("ccdpx", {"setter": detectors.CCDPixel, "getter": detectors.CCDPixel}), ( "cp", { "setter": detectors.CavityPropertyDetector, "getter": detectors.CavityPropertyDetector, }, ), ("fcam", {"setter": detectors.FieldCamera, "getter": detectors.FieldCamera}), ( "fline", {"setter": detectors.FieldScanLine, "getter": detectors.FieldScanLine}, ), ("fpx", {"setter": detectors.FieldPixel, "getter": detectors.FieldPixel}), # Gouy's `build_last` flag is set because it implicitly depends on any nodes # that appear in the path from its start port back to itself, so its # dependencies cannot be determined by the time the first set of elements are # built into the model. It is therefore moved to the second build pass by this # flag. ( "gouy", {"setter": detectors.Gouy, "getter": detectors.Gouy, "build_last": True}, ), ("knmd", {"setter": detectors.KnmDetector, "getter": detectors.KnmDetector}), ( "mmd", { "setter": detectors.ModeMismatchDetector, "getter": detectors.ModeMismatchDetector, }, ), ( ("motion_detector", "xd"), {"setter": detectors.MotionDetector, "getter": detectors.MotionDetector}, ), ( ("power_detector_dc", "pd"), {"setter": detectors.PowerDetector, "getter": detectors.PowerDetector}, ), ( ("power_detector_demod_1", "pd1"), { "setter": detectors.PowerDetectorDemod1, "getter": detectors.PowerDetectorDemod1, }, ), ( ("power_detector_demod_2", "pd2"), { "setter": detectors.PowerDetectorDemod2, "getter": detectors.PowerDetectorDemod2, }, ), ( ("quantum_noise_detector", "qnoised"), { "setter": detectors.QuantumNoiseDetector, "getter": detectors.QuantumNoiseDetector, }, ), ( ("quantum_noise_detector_demod_1", "qnoised1"), { "setter": detectors.QuantumNoiseDetectorDemod1, "getter": detectors.QuantumNoiseDetectorDemod1, }, ), ( ("quantum_noise_detector_demod_2", "qnoised2"), { "setter": detectors.QuantumNoiseDetectorDemod2, "getter": detectors.QuantumNoiseDetectorDemod2, }, ), ( ("quantum_shot_noise_detector", "qshot"), { "setter": detectors.QuantumShotNoiseDetector, "getter": detectors.QuantumShotNoiseDetector, }, ), ( ("quantum_shot_noise_detector_demod_1", "qshot1"), { "setter": detectors.QuantumShotNoiseDetectorDemod1, "getter": detectors.QuantumShotNoiseDetectorDemod1, }, ), ( ("quantum_shot_noise_detector_demod_2", "qshot2"), { "setter": detectors.QuantumShotNoiseDetectorDemod2, "getter": detectors.QuantumShotNoiseDetectorDemod2, }, ), ( ("signal_generator", "sgen"), { "setter": components.SignalGenerator, "getter": components.SignalGenerator, }, ), ("splitpd", {"setter": detectors.SplitPD, "getter": detectors.SplitPD}), ( ("zpk_actuator", "actuator"), { "setter": electronics.ZPKNodeActuator, "getter": electronics.ZPKNodeActuator, }, ), # Connectors. (("space", "s"), {"setter": components.Space, "getter": components.Space}), ("nothing", {"setter": components.Nothing, "getter": components.Nothing}), # Mechanics. ("free_mass", {"setter": mechanical.FreeMass, "getter": mechanical.FreeMass}), ("pendulum", {"setter": mechanical.Pendulum, "getter": mechanical.Pendulum}), ( "ligo_triple", {"setter": ligo.LIGOTripleSuspension, "getter": ligo.LIGOTripleSuspension}, ), ( "ligo_quad", {"setter": ligo.LIGOQuadSuspension, "getter": ligo.LIGOQuadSuspension}, ), # Lock. ("lock", {"setter": locks.Lock, "getter": locks.Lock}), # Noises. ("noise", {"setter": noise.ClassicalNoise, "getter": noise.ClassicalNoise}), # Gauss. ("gauss", {"setter": components.Gauss, "getter": _GuassGetterProxy()},), ] # List of default function adapters. _DEFAULT_COMMANDS = [ ( # Fsig. # This technically sets a component (:class:`.finesse.frequency.Fsig`), but # it's always present in models so this is instead implemented as a command. "fsig", {"setter": _set_fsig, "getter": _FsigGetterProxy(), "singular": True}, ), ( "lambda", {"setter": _set_lambda0, "getter": _LambdaGetterProxy(), "singular": True}, ), ( "modes", { "setter": Model.select_modes, "getter": _ModesGetterProxy(), "singular": True, }, ), ("link", {"setter": _set_link, "singular": False},), ( "intrix", {"setter": _set_intrix, "getter": _IntrixGetterProxy(), "singular": False}, ), ("tem", {"setter": _set_tem, "getter": _TEMGetterProxy(), "singular": False},), ] _DEFAULT_ANALYSES = [ # Group actions. ("parallel", {"setter": actions.Parallel, "getter": actions.Parallel}), ("series", {"setter": actions.Series, "getter": actions.Series}), # Axes. ("noxaxis", {"setter": actions.Noxaxis, "getter": actions.Noxaxis}), ("xaxis", {"setter": actions.Xaxis, "getter": actions.Xaxis}), ("x2axis", {"setter": actions.X2axis, "getter": actions.X2axis}), ("x3axis", {"setter": actions.X3axis, "getter": actions.X3axis}), ("sweep", {"setter": actions.Sweep, "getter": actions.Sweep}), ("change", {"setter": actions.Change, "getter": actions.Change}), ( ("freqresp", "frequency_response"), {"setter": actions.FrequencyResponse, "getter": actions.FrequencyResponse}, ), ( "opt_rf_readout_phase", { "setter": actions.OptimiseRFReadoutPhaseDC, "getter": actions.OptimiseRFReadoutPhaseDC, }, ), ( "sensing_matrix_dc", {"setter": actions.SensingMatrixDC, "getter": actions.SensingMatrixDC}, ), # Model physics. ( "noise_analysis", {"setter": noise.NoiseAnalysis, "getter": noise.NoiseAnalysis}, ), ("abcd", {"setter": actions.ABCD, "getter": actions.ABCD}), ("beam_trace", {"setter": actions.BeamTrace, "getter": actions.BeamTrace}), ( "propagate_beam", {"setter": actions.PropagateBeam, "getter": actions.PropagateBeam}, ), ( "propagate_beam_astig", { "setter": actions.PropagateAstigmaticBeam, "getter": actions.PropagateAstigmaticBeam, }, ), # Utilities. ("debug", {"setter": actions.Debug, "getter": actions.Debug}), ("plot", {"setter": actions.Plot, "getter": actions.Plot}), ("print", {"setter": actions.Printer, "getter": actions.Printer}), ("run_locks", {"setter": actions.RunLocks, "getter": actions.RunLocks}), ( "noise_projection", {"setter": actions.NoiseProjection, "getter": actions.NoiseProjection}, ), ("print_model", {"setter": actions.PrintModel, "getter": actions.PrintModel}), ( "print_model_attr", {"setter": actions.PrintModelAttr, "getter": actions.PrintModelAttr}, ), ] _SUPPORTED_KEYWORDS = { # None. "none", # HOM collections. "even", "odd", "x", "y", "off", # Axis scales. "lin", "log", # Modulator types. "am", "pm", # Filter types. "lowpass", "highpass", "bandpass", "bandstop", "xsplit", "ysplit", # Beam properties (see :class:`finesse.detectors.compute.gaussian.BeamProperty`). *detectors.bpdetector.BP_KEYWORDS.keys(), # Cavity properties (see :class:`finesse.detectors.compute.gaussian.CavityProperty`). *detectors.cavity_detector.CP_KEYWORDS.keys(), } _SUPPORTED_CONSTANTS = symbols.CONSTANTS _SUPPORTED_UNARY_OPERATORS = { "+": symbols.FUNCTIONS["pos"], "-": symbols.FUNCTIONS["neg"], } _SUPPORTED_BINARY_OPERATORS = { "+": symbols.OPERATORS["__add__"], "-": symbols.OPERATORS["__sub__"], "*": symbols.OPERATORS["__mul__"], "**": symbols.OPERATORS["__pow__"], "/": symbols.OPERATORS["__truediv__"], "//": symbols.OPERATORS["__floordiv__"], } # Built-in functions. _SUPPORTED_EXPRESSION_FUNCTIONS = symbols.FUNCTIONS