Source code for atomiq.components.optoelectronics.lightmodulator

from __future__ import annotations

from atomiq.components.primitives import Component, Parametrizable, Switchable
from atomiq.components.electronics.rfsource import RFSource
from atomiq.components.basics.calibration import Calibration

from atomiq.helper import identity_float, replace_member

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


[docs] class Shutter(Component, Switchable): """ Component to switch light on or off depending on a logical signal. This could be a mechanical shutter or a binary only amplitude modulator (e.g. AOM, EOM, Pockels cell etc.). It requires a class:Switchable switch that operates the shutter Args: switch: Switch that operates the shutter, e.g. TTL invert: invert the logic of on and off opening_time: Time in s it takes from the arrival of the TTL until the shutter is fully opened (default 0) closing_time: Time in s it takes from the arrival of the TTL until the shutter is completely closed (default 0). Note that if closing_time is > 0 the shutter is already closing before the time at which the off() method is called """ kernel_invariants = {"switch", "invert"} def __init__(self, switch: Switchable, invert: TBool = False, opening_time: TFloat = 0, closing_time: TFloat = 0, *args, **kwargs): Component.__init__(self, *args, **kwargs) Switchable.__init__(self, ["blank"]) self.switch = switch self.invert = invert self.opening_time = opening_time self.closing_time = closing_time
[docs] @kernel def on(self): """ Opens the shutter. The time cursor is moved back in time to accommodate the `opening_time` of the shutter. After executing the `on` (or `off` if inverted) method of the defined switch the time cursor is then forwarded by `opening_time` again. The time cursor advancement is therefore given by the `switch.on()` or `switch.off()` method. """ delay(-self.opening_time*s) if not self.invert: self.switch.on() else: self.switch.off() delay(self.opening_time*s)
[docs] @kernel def off(self): """ Closes the shutter. The time cursor is moved back in time to accommodate the `closing_time` of the shutter. After executing the `off` (or `on` if inverted) method of the defined switch the time cursor is then forwarded by `closing_time` again. The time cursor advancement is therefore given by the `switch.on()` or `switch.off()` method. """ delay(-self.closing_time*s) if not self.invert: self.switch.off() else: self.switch.on() delay(self.closing_time*s)
[docs] class LightModulator(Component, Parametrizable): """An abstract light modulator for frequency, amplitude, phase, polarisation This class serves as a base class for all kinds of electro-optic devices that can change the properties of light. """ def __init__(self, *args, **kwargs): Component.__init__(self, *args, **kwargs) Parametrizable.__init__(self, ["frequency", "amplitude", "phase"])
[docs] @kernel def set_frequency(self, frequency: TFloat): """Set the frequency by which the light is shifted. Args: frequency: Frequency in Hz by which the light is shifted. """ raise NotImplementedError("Implement `set_frequency` for your light modulator")
[docs] @kernel def get_frequency(self) -> TFloat: raise NotImplementedError("Implement `get_frequency` for your light modulator")
[docs] @kernel def set_amplitude(self, amplitude: TFloat): """Set the amplitude of the light after the modulator Args: amplitude: Relative amplitude [0 .. 1] of the light after the modulator """ raise NotImplementedError("Implement `set_amplitude` for your light modulator")
[docs] @kernel def set_phase(self, phase: TFloat): """Set the phase shift of the light imposed by the modulator Args: phase: Phase shift in radians """ raise NotImplementedError("Implement `set_phase` for your light modulator")
[docs] @kernel def set_polarisation(self, angle: TFloat): """Set the polarization rotation imposed by the modulator Args: angle: Rotation angle in radians """ raise NotImplementedError("Implement `set_polarisation` for your light modulator")
[docs] class RFLightModulator(LightModulator): """A light modulator driven by an RF source This class serves as a base class for devices like AOM, EOM, etc. Args: rfsource: The rfsource that drives the modulator freq_limit: Tuple (freq_min, freq_max) giving the minimum/maximum RF frequency that the modulator can handle in Hz. amp_limit: Tuple (amp_min, amp_max) giving the minimum/maximum RF attenuation that the modulator can do in range [0..1]. """ kernel_invariants = {"rfsource", "freq_min", "freq_max", "amp_min", "amp_max"} def __init__(self, rfsource: RFSource, freq_limit: tuple, amp_limit: tuple = (0.0, 1.0), *args, **kwargs): LightModulator.__init__(self, *args, **kwargs) self.rfsource = rfsource if isinstance(rfsource, Switchable): self.switch = self.rfsource else: self.on = lambda: self.experiment.log.warning("Modulator " + self.identifier + " has no switch that can be used to switch on") self.off = lambda: self.experiment.log.warning("Modulator " + self.identifier + "has no switch that can be used to switch off") self.freq_min, self.freq_max = freq_limit self.amp_min, self.amp_max = amp_limit
[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, ramp_timestep: TFloat = -1.0, ramp_steps: TInt32 = -1): """Ramp frequency and 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. 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 frequency_start >= 0 and (frequency_start < self.freq_min or frequency_start > self.freq_max): self.experiment.log.error("Requested start frequency {0} Hz in ramp outside limits [{1}, {2}] Hz for the \ modulator " + self.identifier, [frequency_start, self.freq_min, self.freq_max]) return if frequency_end >= 0 and (frequency_end < self.freq_min or frequency_end > self.freq_max): self.experiment.log.error("Requested end frequency {0} Hz in ramp outside limits [{1}, {2}] Hz for the \ modulator " + self.identifier, [frequency_end, self.freq_min, self.freq_max]) return self.rfsource.ramp(duration, frequency_start, frequency_end, amplitude_start, amplitude_end, ramp_timestep, ramp_steps)
[docs] @kernel def arb(self, duration: TFloat, samples_amp: TList(TFloat) = [], samples_freq: 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 `prepapre_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_freq: List of frequency samples. If this list is empty (default), the frequency is not modified. 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) """ self._arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared, transform_amp, transform_freq, transform_phase)
@kernel def _arb(self, duration: TFloat, samples_amp: TList(TFloat) = [], samples_freq: 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 ): # check frequency in bounds @kernel def _transform_freq(x: TFloat) -> TFloat: x_trans = transform_freq(x) if x_trans > self.freq_max: self.experiment.log.warning(self.identifier + ": Frequency value {0} above upper bound {1}", [x_trans, self.freq_max]) return self.freq_max elif x_trans < self.freq_min: self.experiment.log.warning(self.identifier + ": Frequency value {0} below lower bound {1}", [x_trans, self.freq_min]) return self.freq_min return transform_freq(x) self.rfsource.arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared, transform_amp, _transform_freq, transform_phase)
[docs] @portable def get_frequency_min(self) -> TFloat: return self.freq_min
[docs] @portable def get_frequency_max(self) -> TFloat: return self.freq_max
[docs] @portable def get_frequency(self) -> TFloat: return self.rfsource.get_frequency()
@kernel def _check_and_set_frequency(self, frequency: TFloat): if frequency >= self.freq_min and frequency <= self.freq_max: self.rfsource.set_frequency(frequency) else: self.experiment.log.error("Requested frequency {0} Hz outside limits [{1}, {2}] Hz for the modulator " + self.identifier, [frequency, self.freq_min, self.freq_max])
[docs] @kernel def set_frequency(self, frequency: TFloat): self._check_and_set_frequency(frequency)
[docs] @portable def get_amplitude_min(self) -> TFloat: return self.amp_min
[docs] @portable def get_amplitude_max(self) -> TFloat: return self.amp_max
[docs] @portable def get_amplitude(self) -> TFloat: return self.rfsource.get_amplitude()
@kernel def _check_and_set_amplitude(self, amplitude: TFloat): if amplitude >= self.amp_min and amplitude <= self.amp_max: self.rfsource.set_amplitude(amplitude) else: self.experiment.log.error("Requested amplitude {0} outside limits [{1}, {2}] for the modulator " + self.identifier, [amplitude, self.amp_min, self.amp_max])
[docs] @kernel def set_amplitude(self, amplitude: TFloat): self._check_and_set_amplitude(amplitude)
[docs] @portable def get_phase(self) -> TFloat: return self.rfsource.get_phase()
[docs] @kernel def set_phase(self, value: TFloat): self.rfsource.set_phase(value)
[docs] @kernel def on(self): self.switch.on()
[docs] @kernel def off(self): self.switch.off()
[docs] class AOM(RFLightModulator, Switchable): """An acousto-optical modulator to alter amplitude, frequency and phase of the light. A component to represent an AOM to attenuate, switch and frequency-shift light. It is controlled by an :class:`~atomiq.components.electronics.rfsource.RFSource` and a :class:`~atomiq.components.primitives.Switchable` to rapidly switch the light on and off. As such, the AOM works also as a (non-perfect) shutter. Args: rfsource: The rf source that drives the AOM center_freq: RF center frequency of the AOM in Hz. freq_limit: Tuple (freq_min, freq_max) giving the minimum/maximum RF frequency that the AOM can handle in Hz. Either freq_limit xor bandwidth must be given bandwidth: RF bandwidth of the AOM around the center frequency in Hz. Either bandwidth xor freq_limit must be given. switch: An optional switch to rapidly switch on and off the AOM. If none is given the rfsource is used to switch. switching_delay: the switching delay of the AOM, i.e. the time it takes from the arrival of the TTL to having full optical power. (default 0) passes: How often does the beam pass the AOM? Singlepass -> 1, Doublepass -> 2 (default 1) order: The diffraction order the AOM is aligned to. -2, -1, 1, 2 ... (default 1) am_calibration: Calibration of RF power vs output power. Typically an inverse sigmoid. (default none) """ kernel_invariants = {"center_freq", "switching_delay", "passes", "order"} def __init__(self, center_freq: TFloat, freq_limit: tuple = None, bandwidth: TFloat = None, switch: Switchable = None, switching_delay: TFloat = 0, passes: TInt32 = 1, order: TInt32 = 1, am_calibration: Calibration = None, *args, **kwargs): if freq_limit is None and bandwidth is None: raise ValueError(f"For component {kwargs['identifier']} either `bandwidth` or `freq_limit` must be set") if bandwidth is not None: freq_limit = (center_freq - bandwidth/2., center_freq + bandwidth/2.) kwargs["freq_limit"] = freq_limit RFLightModulator.__init__(self, *args, **kwargs) Switchable.__init__(self, ["blank"]) # Add the am calibration if one is given if am_calibration is not None: self.am_calibration = am_calibration replace_member(self, "_amplitude_transform", "_amplitude_transform_calibration") else: replace_member(self, "_amplitude_transform", "_amplitude_transform_identity") # Add to Parametrizable self.channels.append("detuning") self.center_freq = center_freq self.switching_delay = switching_delay self.passes = passes self.order = order @kernel def _prerun(self): self.detune(0.0)
[docs] @portable def get_frequency_min(self) -> TFloat: return self.passes*self.order*self.freq_min
[docs] @portable def get_frequency_max(self) -> TFloat: return self.passes*self.order*self.freq_max
[docs] @portable def get_frequency(self) -> TFloat: return self.order*self.passes*self.rfsource.get_frequency()
[docs] @kernel def set_frequency(self, frequency: TFloat): """ Set the frequency shift of the light coming out of the AOM """ self._check_and_set_frequency(frequency/(self.passes*self.order))
[docs] @kernel def set_detuning(self, detuning: TFloat): """ Set the frequency shift of the light coming out of the AOM relative to the center frequency Args: detuning: Detuning from the AOM center frequency in Hz """ self.set_frequency(self.center_freq*self.passes*self.order + detuning)
[docs] @kernel def detune(self, detuning: TFloat): """ Alias for set_detuning() """ self.set_detuning(detuning)
@kernel def _amplitude_transform_calibration(self, amplitude: TFloat) -> TFloat: return self.am_calibration.transform(amplitude) @kernel def _amplitude_transform_identity(self, amplitude: TFloat) -> TFloat: return amplitude
[docs] @kernel def set_amplitude(self, amplitude: TFloat): self._check_and_set_amplitude(self._amplitude_transform(amplitude))
[docs] @kernel def arb(self, duration: TFloat, samples_amp: 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 `prepapre_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_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 center 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_freq = identity_float _transform_det = identity_float def _transform_amp(x): return self._amplitude_transform(transform_amp(x)) if len(samples_det) > 0: def _transform_det(x): return transform_freq(x)/(self.passes*self.order) + self.center_freq self._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.passes*self.order) self._arb(duration, samples_amp, samples_freq, samples_phase, repetitions, prepare_only, run_prepared, _transform_amp, _transform_freq, transform_phase)
[docs] @kernel def on(self): """ Turns on the AOM. The time cursor is moved back in time to accommodate the `switching_delay` of the AOM. After executing the `on` method of the defined switch the time cursor is then forwarded by `switching_delay` again. The time cursor advancement is therefore given by the `switch.on()` method. """ delay(-self.switching_delay*s) self.switch.on() delay(self.switching_delay*s)
[docs] @kernel def off(self): """ Turns off the AOM. The time cursor is moved back in time to accommodate the `switching_delay` of the AOM. After executing the `off` method of the defined switch the time cursor is then forwarded by `switching_delay` again. The time cursor advancement is therefore given by the `switch.off()` method. """ delay(-self.switching_delay*s) self.switch.off() delay(self.switching_delay*s)