"""A component that introduces some loss for particular frequencies or light, relative
to the carrier frequency.
This can be used to introduce sideband imbalance, for example.
"""
import logging
import numpy as np
from finesse.components.general import Connector, InteractionType
from finesse.components.node import NodeType, NodeDirection
from finesse.components.workspace import ConnectorWorkspace
from finesse.parameter import float_parameter
LOGGER = logging.getLogger(__name__)
[docs]
class FrequencyLossWorkspace(ConnectorWorkspace):
    def __init__(self, owner, sim):
        super().__init__(owner, sim, False, False) 
[docs]
@float_parameter("loss", "Loss")
@float_parameter(
    "phase",
    "Phase",
    units="Degrees",
)
@float_parameter(
    "f",
    "Frequency",
    units="Hz",
)
# IMPORTANT: renaming this class impacts the katscript spec and should be avoided!
class FrequencyLoss(Connector):
    """Represents an unphysical element which introduces a loss and/or phase for a
    particular frequency.
    Parameters
    ----------
    name : str
        Name of newly created lens.
    f : float, optional
        Frequency to apply loss and phase to.
    loss : float, optional
        Fractional loss at the frequency
    phase : float, optional
        Phase change at the frequency
    """
    def __init__(self, name, f, loss=0, phase=0):
        super().__init__(name)
        self.f = f
        self.loss = loss
        self.phase = phase
        self._add_port("p1", NodeType.OPTICAL)
        self.p1._add_node("i", NodeDirection.INPUT)
        self.p1._add_node("o", NodeDirection.OUTPUT)
        self._add_port("p2", NodeType.OPTICAL)
        self.p2._add_node("i", NodeDirection.INPUT)
        self.p2._add_node("o", NodeDirection.OUTPUT)
        # optic to optic couplings
        self._register_node_coupling(
            "P1i_P2o",
            self.p1.i,
            self.p2.o,
            interaction_type=InteractionType.TRANSMISSION,
        )
        self._register_node_coupling(
            "P2i_P1o",
            self.p2.i,
            self.p1.o,
            interaction_type=InteractionType.TRANSMISSION,
        )
    def _get_workspace(self, sim):
        ws = FrequencyLossWorkspace(self, sim)
        ws.I = np.eye(sim.model_settings.num_HOMs, dtype=np.complex128)
        ws.carrier.add_fill_function(self._fill_carrier, False)
        ws.signal.add_fill_function(self._fill_signal, False)
        return ws
    def _fill_optical_matrix(self, ws, matrix, connections, signal_fill):
        for freq in matrix.optical_frequencies.frequencies:
            M = ws.I
            # Suppress carrier and it's signal sidebands
            if freq.f == ws.values.f or (
                signal_fill and freq.audio_carrier.f == ws.values.f
            ):
                M *= (1 - ws.values.loss) * np.exp(1j * np.radians(ws.values.phase))
            with matrix.component_edge_fill3(
                ws.owner_id,
                connections.P1i_P2o_idx,
                freq.index,
                freq.index,
            ) as mat:
                mat[:] = M
            with matrix.component_edge_fill3(
                ws.owner_id,
                connections.P2i_P1o_idx,
                freq.index,
                freq.index,
            ) as mat:
                mat[:] = M
    def _fill_carrier(self, ws):
        self._fill_optical_matrix(ws, ws.sim.carrier, ws.carrier.connections, False)
    def _fill_signal(self, ws):
        self._fill_optical_matrix(ws, ws.sim.signal, ws.signal.connections, True)