"""Optical components performing modulation of beams."""
import logging
import finesse
import numpy as np
from finesse.components.general import InteractionType, Connector, FrequencyGenerator
from finesse.components.node import NodeType, NodeDirection
from finesse.parameter import (
float_parameter,
int_parameter,
bool_parameter,
enum_parameter,
ParameterState,
)
from finesse.symbols import Variable, Matrix
from finesse.enums import ModulatorType
LOGGER = logging.getLogger(__name__)
[docs]@float_parameter("f", "Frequency", units="Hz")
@float_parameter("midx", "Modulation index")
@float_parameter("phase", "Phase", units="degrees")
@int_parameter(
"order",
"Maximum modulation order",
changeable_during_simulation=False,
validate="_check_order",
)
@enum_parameter(
"mod_type",
f"Modulation type {tuple(_ for _ in ModulatorType.__members__.keys())}",
ModulatorType,
changeable_during_simulation=False,
validate="_check_mod_type",
)
@bool_parameter("positive_only", "Positive only", changeable_during_simulation=False)
# IMPORTANT: renaming this class impacts the katscript spec and should be avoided!
class Modulator(Connector, FrequencyGenerator):
"""Represents a modulator optical component with associated properties such as
modulation frequency, index and order.
Parameters
----------
name : str
Name of newly created modulator.
f : float or :class:`.Frequency`, optional
Frequency of the modulation (in Hz) or :class:`.Frequency` object.
midx : float
Modulation index, >= 0.
order : int, optional
Maximum order of modulations to produce. Must be 1 for
amplitude modulation. Defaults to 1.
mod_type : str, optional
Modulation type, either 'am' (:ref:`amplitude
modulation<amp_mod>`) or 'pm' (:ref:`phase
modulation<phase_mod>`). Defaults to 'pm'.
phase : float, optional
Relative phase of modulation (in degrees). Defaults to 0.0.
positive_only : bool, optional
If True, only produce positive-frequency sidebands. Defaults to False.
"""
[docs] def __init__(
self,
name,
f,
midx,
order=1,
mod_type=ModulatorType.pm,
phase=0.0,
positive_only=False,
):
Connector.__init__(self, name)
FrequencyGenerator.__init__(self)
self.f = f
self.midx = midx
self.phase = phase
self.order = order
self.mod_type = mod_type
self.positive_only = positive_only
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)
self._add_port("amp", NodeType.ELECTRICAL)
self.amp._add_node("i", NodeDirection.INPUT)
self._add_port("phs", NodeType.ELECTRICAL)
self.phs._add_node("i", NodeDirection.INPUT)
# 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,
)
self._register_node_coupling("amp_P1o", self.amp.i, self.p1.o)
self._register_node_coupling("amp_P2o", self.amp.i, self.p2.o)
[docs] def optical_equations(self):
settings = self._model._settings
with finesse.symbols.simplification():
if self.mod_type == ModulatorType.pm:
jv = finesse.symbols.FUNCTIONS["jv"]
_k_ = Variable("_k_") # modulation order of the operator
# Scalar for a phase modulation operator
scalar = (1j) ** _k_ * jv(_k_, self.midx.ref)
if settings.is_modal:
return {
f"{self.name}.P1i_P2o": scalar * Matrix(f"{self.name}.K12"),
f"{self.name}.P2i_P1o": scalar * Matrix(f"{self.name}.K21"),
}
else:
return {
f"{self.name}.P1i_P2o": scalar,
f"{self.name}.P2i_P1o": scalar,
}
else:
raise NotImplementedError()
def _modulation_frequencies(self):
order = self.order.eval()
if order - int(order) != 0:
raise ValueError(
f"Modulation order for {self.name} must be an integer not {order}"
)
order = int(order)
orders = list(range(-order, 1 + order))
orders.pop(order) # remove 0
fm = np.dot(self.f.ref, orders)
return fm
def _couples_frequency(self, ws, connection, f_in, f_out):
if connection in ("P2i_P1o", "P1i_P2o"):
if f_in == f_out:
return True
elif (f_in, f_out) in ws.carrier_frequency_couplings:
return True
elif (f_out, f_in) in ws.carrier_frequency_couplings:
return True
elif (f_in, f_out) in ws.signal_frequency_couplings:
return True
elif (f_out, f_in) in ws.signal_frequency_couplings:
return True
else:
return False
else: # any other signal connection
return True
def _check_order(self, value):
if self.mod_type == ModulatorType.am and int(value) != 1:
raise ValueError(
"Modulation order must be 1 when using amplitude modulation"
)
return value
def _check_mod_type(self, value):
try:
value = ModulatorType(value)
except ValueError:
try:
value = ModulatorType[value] # cast input into enum
except KeyError:
raise ValueError(
f"'{value}' is not a valid Modulator type, options are {tuple(_ for _ in ModulatorType.__members__.keys())}"
)
if self.order.state == ParameterState.Numeric:
if value == ModulatorType.am and self.order > 1:
raise ValueError("Amplitude modulation can only be used with order 1")
return value
@property
def f(self):
"""Source frequency representing modulation.
Returns
-------
:class:`float` or :class:`.Frequency`
The modulation frequency.
"""
return self.__f
@f.setter
def f(self, value):
self.__f = value
@property
def order(self):
"""The maximum order of modulations produced by the modulator.
Returns
-------
int
The maximum modulation order.
"""
return self.__order
@order.setter
def order(self, value):
self.__order = int(value)
def _get_workspace(self, sim):
from finesse.components.modal.modulator import (
ModulatorWorkspace,
modulator_carrier_fill,
modulator_signal_optical_fill,
modulator_signal_phase_fill,
modulator_signal_amp_fill,
)
ws = ModulatorWorkspace(self, sim)
ws.carrier.add_fill_function(
modulator_carrier_fill, self.midx.is_changing or self.phase.is_changing
)
if sim.signal:
# modulator for signals doesn't actually need refilling each time if
# signal frequency is changing, as the coupling depends on the
ws.signal.add_fill_function(
modulator_signal_optical_fill,
self.midx.is_changing or self.phase.is_changing,
)
if ws.amp_signal_enabled:
# Amplitude signal elements do not depend on the signal frequency
ws.signal.add_fill_function(
modulator_signal_amp_fill,
self.midx.is_changing or self.phase.is_changing,
)
if ws.phs_signal_enabled:
# Phase signal elements do not depend on the signal frequency
ws.signal.add_fill_function(
modulator_signal_phase_fill,
self.midx.is_changing or self.phase.is_changing,
)
return ws