from __future__ import annotations
import numpy as np
from atomiq.components.primitives import Component, Parametrizable
from atomiq.components.electronics.rfsource import RFSource
from atomiq.components.optoelectronics.wavelengthmeter import StabilizingWavelengthMeterChannel
from atomiq.helper import isnan
from artiq.experiment import kernel, portable, delay
from artiq.language.units import s
[docs]
class Lock(Component):
"""
This is a very generic class to describe a laser lock. It is characterized by some means
of a reference frequency and an offset to that frequency at which the lock tries to stabilize.
Most likely you want to use a more specific class inherited from this one.
All frequencies of this class refer to the fundamental of the laser, i.e. the frequency the lock acts on.
:meth:`get_frequency` returns the frequency of the light exiting the laser, i.e. the fundamental frequency
multiplied by the ``harmonic``.
Args:
reference_frequency: The frequency in Hz that the lock references to (at the laser fundamental)
lock_offset: The frequency offset in Hz at which the lock stabilizes (at the laser fundamental)
harmonic: Use this option if the laser is frequency converted but locked to the fundamental. Use 1 for no
conversion, 2 for SHG, 4 for FHG ,... (default 1)
"""
kernel_invariants = {"harmonic"}
def __init__(
self, reference_frequency: float, lock_offset: float = 0.0, harmonic: np.int32 = np.int32(1), *args, **kwargs
):
Component.__init__(self, *args, **kwargs)
self._lock_offset = lock_offset
self.reference_frequency = reference_frequency
self.harmonic = harmonic
[docs]
@portable
def get_frequency(self) -> float:
"""Get the absolute frequency of the light exiting the laser in Hz (fundamental frequency x harmonic)"""
return (self.reference_frequency + self._lock_offset) * self.harmonic
[docs]
class DetunableLock(Lock, Parametrizable):
"""
This is a very generic class to describe a laser lock where the lock offset can be
changed. It is characterized by some means of a reference frequency and a detunable
offset to that frequency at which the lock tries to stabilize.
Most likely you want to use a more specific class inherited from this one.
Args:
settle_time: Time in s the lock needs to settle after a detuning of the lock point
blind: Whether the lock is set to its default values during each prerun phase. If `True`, the lock is unchanged
in the prerun phase. Defaults to `False`.
"""
kernel_invariants = {"settle_time"}
# blind selects at build time whether _prerun is replaced by a no-op
_shape_changing_arguments = ("blind",)
def __init__(self, settle_time: float = 0, blind: bool = False, *args, **kwargs):
Lock.__init__(self, *args, **kwargs)
Parametrizable.__init__(self, ["frequency", "detuning"])
self.settle_time = settle_time
@kernel
def do_nothing(self):
pass
if blind:
self._prerun = do_nothing.__get__(self)
[docs]
@kernel
def _prerun(self):
self.set_detuning(self._lock_offset)
[docs]
@portable
def _set_lock_frequency(self, detuning: float):
"""Set the lock frequency from a detuning at the laser fundamental (implement in a subclass)"""
raise NotImplementedError("Abstract class DetunableLaser cannot set lock frequency. Implement it in a subclass")
[docs]
@portable
def _ramp_lock_frequency(
self, duration: float, detuning_start: float = float("nan"), detuning_end: float = float("nan")
):
"""Ramp the lock frequency from detunings at the laser fundamental (implement in a subclass)"""
raise NotImplementedError("Abstract class DetunableLaser cannot set ramp frequency. Implement it in a subclass")
[docs]
@portable
def set_frequency(self, frequency: float):
"""Set the absolute frequency of the light exiting the laser
Args:
frequency: The absolute frequency in Hz of the light exiting the laser
"""
self.set_detuning(frequency / self.harmonic - self.reference_frequency)
[docs]
@portable
def set_detuning(self, offset_frequency: float):
"""
Set the frequency of the laser fundamental relative to the reference frequency, i.e. set the lock offset.
Args:
offset_frequency: new lock offset in Hz (at the laser fundamental)
"""
self._lock_offset = offset_frequency
if self.debug_output:
self.experiment.log.info(self.identifier + ": New lock offset {0:.3f} MHz", [self._lock_offset / 1e6])
self._set_lock_frequency(offset_frequency)
# wait for the lock to settle
delay(self.settle_time * s)
[docs]
@portable
def ramp_detuning(
self,
duration: float,
offset_frequency_start: float = float("nan"),
offset_frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = np.int32(-1),
):
"""Ramp the lock offset, i.e. the detuning of the laser fundamental from the reference frequency
Args:
duration: Ramp duration in s
offset_frequency_start: Initial lock offset in Hz (at the laser fundamental)
offset_frequency_end: Final lock offset in Hz (at the laser fundamental)
ramp_timestep: Time in s between two ramp steps
ramp_steps: Number of ramp steps (default -1, i.e. derive from the timestep)
"""
assert not isnan(offset_frequency_end)
self._ramp_lock_frequency(
duration,
frequency_start=offset_frequency_start,
frequency_end=offset_frequency_end,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
self._lock_offset = offset_frequency_end
[docs]
@portable
def ramp_frequency(
self,
duration: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = np.int32(-1),
):
"""Ramp the absolute frequency of the light exiting the laser
Args:
duration: Ramp duration in s
frequency_start: Initial absolute frequency in Hz of the light exiting the laser
frequency_end: Final absolute frequency in Hz of the light exiting the laser
ramp_timestep: Time in s between two ramp steps
ramp_steps: Number of ramp steps (default -1, i.e. derive from the timestep)
"""
detuning_start = (
frequency_start / self.harmonic - self.reference_frequency
if not isnan(frequency_start)
else frequency_start
)
detuning_end = (
frequency_end / self.harmonic - self.reference_frequency if not isnan(frequency_end) else frequency_end
)
self.ramp_detuning(
duration,
offset_frequency_start=detuning_start,
offset_frequency_end=detuning_end,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
[docs]
class SpectroscopyLock(Lock):
pass
[docs]
class SidebandLock(DetunableLock):
"""Lock on a sideband modulated onto the laser
This kind of lock is frequently used when locking on tunable sideband to the transmission
signal of a stable cavity. The offset frequency for the sideband is generated by an RF source
and can be changed at runtime. Thus the lock point can be detuned.
Args:
rf_source: RF source that determines the sideband frequency.
sideband_order: Order of the sideband the laser is locked to [.., -2, -1, 1, 2, ...] (default 1)
"""
kernel_invariants = {"rf_source", "sideband_order"}
def __init__(self, rf_source: RFSource, sideband_order: np.int32 = np.int32(1), *args, **kwargs):
self.rf_source = rf_source
self.sideband_order = sideband_order
DetunableLock.__init__(self, *args, **kwargs)
[docs]
@portable
def _set_lock_frequency(self, frequency: float):
"""Set the RF source frequency (fundamental detuning / sideband_order)"""
self.rf_source.set_frequency(frequency / self.sideband_order)
[docs]
@portable
def _ramp_lock_frequency(
self,
duration: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = np.int32(-1),
):
"""Ramp the RF source frequency (fundamental detunings / sideband_order)"""
self.rf_source.ramp(
duration=duration,
frequency_start=frequency_start / self.sideband_order,
frequency_end=frequency_end / self.sideband_order,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
[docs]
class OFCLock(DetunableLock):
"""Lock on an optical frequency comb
This is used to lock the laser on a beat note with an optical frequency comb. It is characterized by the
frequency of the closest comb tooth and the offset frequency. The offset frequency is generated by an RF source
and can be changed at runtime. Thus the lock point can be detuned.
Args:
rfsource: RF source that generates the reference beat frequency (i.e. the offset frequency) to which the laser
beat note is stabilized.
reference_frequency: The frequency in Hz of the comb tooth. Instead ``ceo``, ``rep_rate`` and
``tooth_number``) can be passed to automatically calculate the reference frequency.
``reference_frequency`` is ignored in this case.
lock_offset: The default frequency offset to the comb tooth in Hz at which the lock stabilizes
settle_time: Time in s the lock needs to settle after a detuning of the lock point
lock_direction: Whether the laser is locked to the positive (+1) or negative (-1) beat note (default 1)
tooth_number: Number of the comb tooth, the laser is beaten with (default -1)
rep_rate: Repetition frequency of the comb in Hz (default ``nan``)
ceo: Ceo frequency of the comb in Hz (default ``nan``)
"""
kernel_invariants = {"rf_source", "lock_direction"}
def __init__(
self,
rf_source: RFSource,
lock_direction: np.int32 = np.int32(1),
tooth_number: np.int32 = np.int32(-1),
rep_rate: float = float("nan"),
ceo: float = float("nan"),
*args,
**kwargs,
):
self.rf_source = rf_source
self.lock_direction = lock_direction
if not isnan(ceo) and not isnan(rep_rate) and tooth_number > 0:
kwargs["reference_frequency"] = tooth_number * rep_rate + ceo
DetunableLock.__init__(self, *args, **kwargs)
[docs]
@portable
def _set_lock_frequency(self, frequency: float):
"""Set the RF source frequency (fundamental detuning / lock_direction)"""
self.rf_source.set_frequency(frequency / self.lock_direction)
[docs]
@portable
def _ramp_lock_frequency(
self,
duration: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = np.int32(-1),
):
"""Ramp the RF source frequency (fundamental detunings / lock_direction)"""
self.rf_source.ramp(
duration=duration,
frequency_start=frequency_start / self.lock_direction,
frequency_end=frequency_end / self.lock_direction,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
[docs]
class OffsetLock(SidebandLock):
"""Lock on a reference laser via beating signal
Args:
reference_laser: laser that is used to generate the beat note. It's frequency serves as the reference frequency
for this lock
"""
kernel_invariants = {"reference_laser"}
def __init__(self, reference_laser, *args, **kwargs):
self.reference_laser = reference_laser
kwargs["reference_frequency"] = self.reference_laser.get_frequency()
SidebandLock.__init__(self, *args, **kwargs)
[docs]
@portable
def get_frequency(self) -> float:
"""Get the absolute frequency of the light exiting the laser in Hz, based on the current
frequency of the reference laser"""
return self.harmonic * (self.reference_laser.get_frequency() + self._lock_offset)
[docs]
@portable
def set_frequency(self, frequency: float):
"""Set the absolute frequency of the light exiting the laser, relative to the current
frequency of the reference laser
Args:
frequency: The absolute frequency in Hz of the light exiting the laser
"""
self.set_detuning(frequency / self.harmonic - self.reference_laser.get_frequency())
[docs]
@portable
def ramp_frequency(
self,
duration: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = -1,
):
"""Ramp the absolute frequency of the light exiting the laser, relative to the current
frequency of the reference laser
Args:
duration: Ramp duration in s
frequency_start: Initial absolute frequency in Hz of the light exiting the laser
frequency_end: Final absolute frequency in Hz of the light exiting the laser
ramp_timestep: Time in s between two ramp steps
ramp_steps: Number of ramp steps (default -1, i.e. derive from the timestep)
"""
if not isnan(frequency_start):
detuning_start = frequency_start / self.harmonic - self.reference_laser.get_frequency()
else:
detuning_start = frequency_start
if not isnan(frequency_end):
detuning_end = frequency_end / self.harmonic - self.reference_laser.get_frequency()
else:
detuning_end = frequency_end
self.ramp_detuning(
duration,
offset_frequency_start=detuning_start,
offset_frequency_end=detuning_end,
ramp_timestep=ramp_timestep,
ramp_steps=ramp_steps,
)
[docs]
class WavelengthMeterLock(DetunableLock):
"""Lock a laser to an absolute frequency using a wavelength meter
This lock uses a :class:`StabilizingWavelengthMeterChannel` to measure and stabilize the laser frequency.
Args:
wavemeter: The stabilizing wavelength meter channel used to measure and control the laser frequency.
reference_frequency: The absolute frequency in Hz at which the laser should be locked.
lock_offset: The default frequency offset in Hz from the reference frequency (default 0).
harmonic: Use this option if the laser is frequency converted but locked to the fundamental.
Use 1 for no conversion, 2 for SHG, 4 for FHG, ... (default 1).
settle_time: Time in s the lock needs to settle after a detuning of the lock point.
"""
kernel_invariants = {"wavemeter"}
def __init__(self, wavemeter: StabilizingWavelengthMeterChannel, *args, **kwargs):
self.wavemeter = wavemeter
DetunableLock.__init__(self, *args, **kwargs)
[docs]
@portable
def _set_lock_frequency(self, frequency: float):
self.wavemeter.set_frequency(self.reference_frequency + frequency * self.harmonic)
[docs]
@portable
def _ramp_lock_frequency(
self,
duration: float,
frequency_start: float = float("nan"),
frequency_end: float = float("nan"),
ramp_timestep: float = float("nan"),
ramp_steps: np.int32 = -1,
):
raise NotImplementedError("WavelengthMeterLock does not support frequency ramps.")