Source code for atomiq.components.laser

from __future__ import annotations

from atomiq.components.primitives import Component, Switchable, Parametrizable, Measurable
from atomiq.components.lock import Lock, DetunableLock
from atomiq.components.optoelectronics.lightmodulator import LightModulator
from atomiq.components.optoelectronics.photodiode import Photodiode
from atomiq.components.optimizers import Optimizer

from atomiq.helper import identity_float, replace_member

from artiq.experiment import portable, kernel, delay
from artiq.language.types import TFloat, TStr, TBool, TInt32, TList
from artiq.language.units import ms, us

import numpy as np


[docs] class LaserSource(Component): """Free-running laser source This class represents a generic (free-running) laser source that emits light of a specified frequency Args: frequency: Frequency of the emitted light in Hz power: Emitted light power in W """ def __init__(self, frequency: TFloat, power: TFloat = 1e-3, *args, **kwargs): Component.__init__(self, *args, **kwargs) self._frequency = frequency self._power = power
[docs] @portable def get_frequency(self): return self._frequency
[docs] @portable def set_frequency(self, frequency: TFloat): self.experiment.log.error("The component " + self.identifier + " doesn't support setting the frequency")
[docs] @portable def get_power(self): return self._power
[docs] @portable def set_power(self, power: TFloat): self.experiment.log.error("The component " + self.identifier + " doesn't support setting the power")
[docs] class LockedLaserSource(LaserSource): """Locked laser source This class represents a generic locked laser source that emits light of a specified frequency Args: lock: lock component that locks the laser source """ kernel_invariants = {"lock"} def __init__(self, lock: Lock, *args, **kwargs): self.lock = lock LaserSource.__init__( self, frequency=self.get_frequency(), *args, **kwargs)
[docs] @portable def get_frequency(self): return self.lock.get_frequency()
[docs] @portable def set_frequency(self, frequency: TFloat): self.lock.set_frequency(frequency)
[docs] class Laser(LaserSource): """Laser light that can be modulated Since we inherit from LaserSource here, a Laser object can itself again be a LaserSource for another Laser object. This way one can describe consecutive manipulations from a LaserSource to the final Laser object. Args: laser_source: The laser source that this laser is taken from zero_freq: absolute frequency in Hz that should be considered zero. This allows to reference the laser frequency relative to e.g. an atomic transition. If not set, the detuning will be relative to the lock point of the laser source """ kernel_invariants = {"laser_source", "zero_freq"} def __init__(self, laser_source: LaserSource, zero_freq: TFloat = None, *args, **kwargs): kwargs["frequency"] = self.get_frequency() LaserSource.__init__(self, *args, **kwargs) self.laser_source = laser_source self.zero_freq = zero_freq if zero_freq is not None else self.laser_source.get_frequency() if not (isinstance(laser_source, LockedLaserSource) and isinstance(laser_source.lock, DetunableLock)) \ and not isinstance(laser_source, Laser): replace_member(self, "set_frequency", "_set_frequency_dummy")
[docs] @portable def get_frequency(self): return self.laser_source.get_frequency()
[docs] @kernel def set_frequency(self, frequency: TFloat): """Set the absolute frequency of the light after modulation Args: frequency: Absolute frequency of the light after modulation in Hz """ self.experiment.log.info(self.identifier + ": Setting frequency to {0:.6f} THz via " + self.laser_source.identifier, [frequency/1e12]) self.laser_source.set_frequency(frequency)
def _set_frequency_dummy(self, frequency: TFloat): self.experiment.log.warning(self.identifier + " cannot be detuned!")
[docs] @kernel def set_detuning(self, detuning: TFloat): """ Set the detuning of the light from the frequency defined by `zero_freq` .. note:: If you are using a modulator (e.g. AOM) the center frequency is not taken into account. If you want to set the detuning from the center frequency of the modulator use `self.modulator.set_detuning` Args: detuning: Detuning in Hz """ self.set_frequency(self.zero_freq + detuning)
[docs] @kernel def detune(self, frequency: TFloat): """ Alias for :meth:`set_detuning` """ self.set_detuning(frequency)
[docs] class SwitchableLaser(Laser, Switchable): """A laser that can be switched on and off This class represents a laser that can be switched on and off, e.g. by a mechanical shutter. However, every `Switchable` can be used to switch the laser. Args: laser_source: The laser source that this laser is taken from switch: The component that switches the laser light """ kernel_invariants = {"switch"} def __init__(self, switch: Switchable, *args, **kwargs): self.laser_source = kwargs["laser_source"] Laser.__init__(self, *args, **kwargs) Switchable.__init__(self, ["switch"]) self.switch = switch
[docs] @kernel def on(self): self.switch.on()
[docs] @kernel def off(self): self.switch.off()
[docs] class ModulatedLaser(SwitchableLaser, Parametrizable): """A laser that can be changed in frequency and amplitude This class represents a laser that is altered by an acusto-optic modular (AOM). The AOM can be used the modulate the amplitude and the frequency of the light. Args: laser_source: The laser source that this laser is taken from switch: The component that switches the laser light. If none is given, the AOM is used to switch the light. modulator: component that does the amplitude/frequency/phase modulation of the laser. E.g. an AOM, EOM, etc. zero_freq: absolute frequency in Hz that should be considered zero. This allows to reference the laser frequency relative to e.g. an atomic transition. If not set, the detuning will be relative to the lock point of the laser source fm_device: Choose whether the frequency should be set via the modulator (mod) or the laser source (src). (default: mod) am_device: Choose whether the power should be set via the modulator (mod) or the laser source (src). (default: mod) src_transmission: Transmission of the light from the laser source to behind the modulator [0..1] (default 1) """ kernel_invariants = {"modulator", "switch", "fm_device", "am_device", "src_transmission"} def __init__(self, modulator: LightModulator, switch: Switchable = None, fm_device: TStr = "mod", am_device: TStr = "mod", src_transmission: TFloat = 1.0, *args, **kwargs): self.modulator = modulator self.laser_source = kwargs["laser_source"] # TODO: I have no guarantee that modulator is switchable kwargs["switch"] = switch if switch is not None else modulator SwitchableLaser.__init__(self, *args, **kwargs) Parametrizable.__init__(self, ["detuning", "frequency", "amplitude"]) if fm_device == "mod": replace_member(self, "set_frequency", "_set_frequency_through_modulator") else: replace_member(self, "set_frequency", "_set_frequency_through_source") self.fm_device = fm_device self.am_device = am_device self.src_transmission = src_transmission
[docs] @kernel def set_amplitude(self, amplitude: TFloat): self.modulator.set_amplitude(amplitude)
[docs] @portable def get_frequency(self) -> TFloat: return self.laser_source.get_frequency() + self.modulator.get_frequency()
[docs] @kernel def set_frequency(self, frequency: TFloat): """Set the absolute frequency of the light after modulation Args: frequency: Absolute frequency of the light after modulation in Hz """ pass
@kernel def _set_frequency_through_modulator(self, frequency: TFloat): self.modulator.set_frequency(frequency - self.laser_source.get_frequency()) @kernel def _set_frequency_through_source(self, frequency: TFloat): self.experiment.log.info("Setting detuning via source for " + self.identifier) self.laser_source.set_frequency(frequency - self.modulator.get_frequency())
[docs] @kernel def ramp( self, duration: TFloat, frequency_start: TFloat = -1.0, frequency_end: TFloat = -1.0, amplitude_start: TFloat = -1.0, amplitude_end: TFloat = -1.0, power_start: TFloat = -1.0, power_end: TFloat = -1.0, ramp_timestep: TFloat = -1.0, ramp_steps: TInt32 = -1 ): """Ramp frequency and/or power/amplitude over a given duration. Parameters default to -1 to indicate no change. If the start frequency/amplitude is set to -1, the ramp starts from the last frequency/amplitude which was set. Either power or amplitude can be given to ramp the intensity of the laser. If power is given, it overwrites the value for the amplitude This method advances the timeline by ´duration´ Args: duration: ramp duration [s] frequency_start: initial frequency [Hz] frequency_end: end frequency [Hz] amplitude_start: initial amplitude [Hz] amplitude_end: end amplitude [Hz] """ if power_start >= 0 and power_end >= 0: amplitude_start = self._amplitude_from_power(power_start) amplitude_end = self._amplitude_from_power(power_end) if self.am_device == "mod" and self.fm_device == "mod" \ or (self.am_device == "mod" and self.fm_device == "src" and (amplitude_end + amplitude_start < 0)) \ or (self.am_device == "src" and self.fm_device == "mod" and (frequency_end + frequency_start < 0)): self.modulator.ramp(duration, frequency_start, frequency_end, amplitude_start, amplitude_end, ramp_timestep, ramp_steps) else: self.experiment.log.error(self.identifier + "Ramping frequency or power via laser source not supported")
[docs] @kernel def arb(self, duration: TFloat, samples_amp: TList(TFloat) = [], samples_power: TList(TFloat) = [], samples_freq: TList(TFloat) = [], samples_det: TList(TFloat) = [], samples_phase: TList(TFloat) = [], repetitions: TInt32 = 1, prepare_only: TBool = False, run_prepared: TBool = False, transform_amp=identity_float, transform_freq=identity_float, transform_phase=identity_float ): """Play Arbitrary Samples from a List This method allows to set the output amplitude, frequency an phase according to the values specified in respective lists. The whole sequence is played in the specified duration. The pattern store in the sample list can also be repeated. We supports a scheme to prepare the arb function before it is actually used. If that is needed, run this function with `prepare_only = True` when the arb should be prepared and with `run_only = True` when the prepared arb should be played. In both calls the other parameters have to be passed. Args: samples_amp: List of amplitude samples. If this list is empty (default), the amplitude is not modified. samples_power: List of power samples. If this list is empty (default), the amplitude is not modified. This overwrites `samples_amp`. samples_freq: List of frequency samples. If this list is empty (default), the frequency is not modified. samples_det: List of frequency samples relative to the zero frequency. If this list is empty (default), the frequency is not modified. This overwrites `samples_frequency` samples_phase: List of phase samples. If this list is empty (default), the phase is not modified. duration: The time in which the whole sequence of samples should be played back [s]. repetitions: Number of times the sequence of all samples should be played. (default 1) """ _transform_power = identity_float _transform_freq = identity_float _transform_det = identity_float if (self.am_device == "src" and len(samples_det) > 0) \ or (self.fm_device == "src" and (len(samples_freq) > 0 or len(samples_det) > 0)): self.experiment.log.error(self.identifier + ": ARB for frequency or power via laser source not supported") return def _transform_power(x): return self._amplitude_from_power(transform_amp(x)) if len(samples_det) > 0: def _transform_det(x): return self.zero_freq + transform_freq(x) - self.laser_source.get_frequency() if len(samples_power) > 0: self.modulator.arb(duration, samples_power, samples_det, [], samples_phase, repetitions, prepare_only, run_prepared, _transform_power, _transform_det, transform_phase) else: self.modulator.arb(duration, samples_amp, samples_det, [], samples_phase, repetitions, prepare_only, run_prepared, transform_amp, _transform_det, transform_phase) else: def _transform_freq(x): return transform_freq(x) - self.laser_source.get_frequency() if len(samples_power) > 0: self.modulator.arb(duration, samples_power, samples_freq, [], samples_phase, repetitions, prepare_only, run_prepared, _transform_power, _transform_freq, transform_phase) else: self.modulator.arb(duration, samples_amp, samples_freq, [], samples_phase, repetitions, prepare_only, run_prepared, transform_amp, _transform_freq, transform_phase)
[docs] @portable def get_power(self) -> TFloat: return self.laser_source.get_power() * self.modulator.get_amplitude()
@portable def _amplitude_from_power(self, power: TFloat) -> TFloat: return power / self.src_transmission / self.laser_source.get_power()
[docs] @kernel def set_power(self, power: TFloat): """Set the absolute power of the light after modulation Args: power: Absolute power of the light after modulation in W """ if self.am_device == "mod": self.modulator.set_amplitude(self._amplitude_from_power(power)) if self.am_device == "src": self.experiment.log.info("Setting power via source for " + self.identifier) self.laser_source.set_power(power / self.src_transmission / self.modulator.get_amplitude())
[docs] class StabilizedModulatedLaser(ModulatedLaser, Measurable): """A power-stabilized modulated laser With some means of measuring the laser power after the amplitude modulator and a feedback, the laser power can be stabilized. This is an abstract class to represent this kind of setups. To monitor the power of the laser after the modulator, a photodiode is required. Args: photodiode: The photodiode that monitors the laser power """ kernel_invariants = {"photodiode"} def __init__(self, photodiode: Photodiode, *args, **kwargs): ModulatedLaser.__init__(self, *args, **kwargs) Measurable.__init__(self, ["power"]) self.photodiode = photodiode
[docs] @kernel def get_power(self) -> TFloat: return self.photodiode.get_power()
[docs] @portable def stabilize(self, enable: TBool): self._stabilize(enable)
def _stabilize(self, enable: TBool): raise NotImplementedError()
[docs] class AutoCalibratedModulatedLaser(StabilizedModulatedLaser): """A laser that uses a Measurable to auto-calibrate its power With some means of measuring the laser power after the amplitude modulator and a feedback, the amplitude for the modulator to achieve a certain power in the beam can be auto-calibrated. To achieve this, this laser measures a configurable number of points and makes a linear interpolation between them if a power in between is requested. Args: points: number of points to measure for the auto-calibration. (default 6) amp_min: minimum amplitude to test in the auto-calibration amp_max: maximum amplitude to test in the auto-calibration """ kernel_invariants = {"points", "amp_min", "amp_max"} def __init__( self, points: TInt32 = 6, *args, amp_min: TFloat = 0.0, amp_max: TFloat = 1.0, timestep: TFloat = 325 * us, **kwargs, ): StabilizedModulatedLaser.__init__(self, *args, **kwargs) self.points = points self.amp_min = amp_min self.amp_max = amp_max self.timestep = timestep # the cache carries entries of the form [power, amplitude] self.cache = np.array([[-1.0, -1.0]] * (points + 1)) self.cache_filled = False
[docs] @portable def clear_cache(self): self.cache_filled = False
@kernel(flags={"fast-math"}) def _cache_closest_idx(self, power: TFloat) -> TInt32: min_dist = 1.8e308 min_idx = -1 for i in range(len(self.cache)): dist = abs(power - self.cache[i][0]) if dist < min_dist: min_idx = i min_dist = dist return min_idx
[docs] @portable(flags={"fast-math"}) def amplitude_from_power(self, power: TFloat) -> TFloat: if self.cache_filled: idx = self._cache_closest_idx(power) if power > self.cache[idx][0]: idl = idx idh = idl + 1 else: idl = idx - 1 idh = idx try: amp = (self.cache[idl][1] + (power - self.cache[idl][0]) / (self.cache[idh][0] - self.cache[idl][0]) * (self.cache[idh][1] - self.cache[idl][1])) except ZeroDivisionError: amp = self.cache[idx][1] / self.cache[idx][0] * power return amp if amp > 0 else 0.0 else: self.experiment.log.error( self.identifier + ": Cannot determine amplitude from power since cache is empty" ) return 0.0
[docs] @kernel def autocalibrate(self): stepsize = (self.amp_max - self.amp_min) / self.points self.on() delay(10 * us) for i in range(self.points + 1): _amp = self.amp_min + i * stepsize self.set_amplitude(_amp) delay(self.timestep) _power = self.get_power() delay(1 * ms) self.cache[i][0] = _power self.cache[i][1] = _amp delay(self.timestep) self.off() self.cache_filled = True
[docs] @kernel def set_power(self, power: TFloat): """Set the output power of the laser If no autocalibration has been done before in this kernel, this will immediately perform the calibration and set the modulator according to the result. If a calibration is present in the cache, the cache values are used to calculate an interpolation. Args: power: the power the laser should be stabilized to. If the photodiode is calibrated the power should be given in the calibrated units (e.g. mW). """ if not self.cache_filled: self.autocalibrate() self.modulator.set_amplitude(self.amplitude_from_power(power))
[docs] class OptimizerStabilizedModulatedLaser(StabilizedModulatedLaser): """A modulated laser power-stabilized through an optimizer This class integrates an optimizer to find the best setting of the amplitude modulator to achieve the desired power on the monitoring photodiode. This can be used to create "poor man's PID". Upon calling :func:set_power() it is checked whether an optimization result for the requested power already exists. If not the optimizer is called to find the best setting. This result is applied to the modulator and stored in the cache. To find the best setting, the optimizer will (multiple times) switch on the laser, change its power and measure the photodiode. When switching on the light interferes with the experimental sequence, make sure to call :func:set_power() for your desired power for the first time before the experiment starts. Then the optimization will be done when it does not interfere with your experiment and at a later time the optimization result can be read from the cache, i.e. not further optimization is possible. Of course this assumes that the laser source power is reasonably stable during the time between the optimization the the time the value is recalled from the cache. Args: optimizer: Optimizer to use for finding the proper setting of the modulator. Make sure that the `actor_component` of the optimizer is identical to the the lasers `modulator` and the optimizer's `monitor_component` is identical to the lasers `photodiode`. cache_size: number of optimization results that can be stored. (default 8) """ kernel_invariants = {"optimizer", "cache_size"} def __init__(self, optimizer: Optimizer, cache_size: TInt32 = 8, *args, **kwargs): StabilizedModulatedLaser.__init__(self, *args, **kwargs) # the cache carries entries of the form [power, amplitude] self.cache = np.array([[-1.0, -1.0]]*cache_size) self.cache_size = cache_size self.clear_cache() self.optimizer = optimizer if self.optimizer.actor != self.modulator: self.experiment.log.warning(self.identifier + ": The optimizer " + self.optimizer.identifier + " does not act on my modulator " + self.modulator.identifier) if self.optimizer.monitor != self.photodiode: self.experiment.log.warning(self.identifier + ": The optimizer " + self.optimizer.identifier + " does not monitor my photodiode " + self.photodiode.identifier) @kernel def _cache_idx(self, power: TFloat) -> TInt32: for i in range(self.cache_len): if self.cache[i][0] == power: return i return -1
[docs] @portable def clear_cache(self): self.cache_len = 0 self.cache_count = 0
[docs] @kernel def set_power(self, power: TFloat): """Set the output power of the laser If the requested power is not already in the cache, this will immediately perform the optimization and set the result on the modulator. If the requested power is present in the cache, the cache value is used. Args: power: the power the laser should be stabilized to. If the photodiode is calibrated the power should be given in the calibrated units (e.g. mW). """ cache_idx = self._cache_idx(power) if cache_idx < 0: cache_idx = self.cache_count % self.cache_size self.cache[cache_idx][0] = power self.cache[cache_idx][1] = self.optimizer.optimize(power) self.cache_count += 1 self.cache_len = min(self.cache_len + 1, self.cache_size) self.modulator.set_amplitude(self.cache[cache_idx][1])
[docs] class ContinuouslyStabilizedModulatedLaser(StabilizedModulatedLaser): """A modulated laser power-stabilized through a continuous servo loop If a laser is power-stabilized by an external (hardware) servo loop, it is represented by this abstract class. Args: start_stabilized: Should the servo loop be activated on startup? (default True) """ def __init__(self, start_stabilized: TBool = True, *args, **kwargs): StabilizedModulatedLaser.__init__(self, *args, **kwargs) self.stabilized = start_stabilized @kernel def _prerun(self): self.stabilize(self.stabilized)