Source code for silq.pulses.pulse_sequences

from functools import partial
import numpy as np
import logging
from collections import Iterable
from .pulse_modules import PulseSequence
from .pulse_types import DCPulse, SinePulse, FrequencyRampPulse, Pulse
from copy import deepcopy


logger = logging.getLogger(__name__)


[docs]class PulseSequenceGenerator(PulseSequence): """Base class for a `PulseSequence` that is generated from settings. """ def __init__(self, pulses=[], **kwargs): super().__init__(pulses=pulses, **kwargs) self.pulse_settings = {} self._latest_pulse_settings = None def __setattr__(self, key, value): super().__setattr__(key, value) # Update value in pulse settings if it exists try: if key in self.pulse_settings: self.pulse_settings[key] = value except AttributeError: pass
[docs] def generate(self): raise NotImplementedError('Needs to be implemented in subclass')
[docs] def up_to_date(self): # Compare to attributes when pulse sequence was created return self.pulse_settings == self._latest_pulse_settings
[docs]class ESRPulseSequence(PulseSequenceGenerator): """`PulseSequenceGenerator` for electron spin resonance (ESR). This pulse sequence can handle many of the basic pulse sequences involving ESR. The pulse sequence is generated from its pulse settings attributes. In general the pulse sequence is as follows: 1. Perform any pre_pulses defined in ``ESRPulseSequence.pre_pulses``. 2. Perform stage pulse ``ESRPulseSequence.ESR['stage_pulse']``. By default, this is the ``plunge`` pulse. 3. Perform ESR pulse within plunge pulse, the delay from start of plunge pulse is defined in ``ESRPulseSequence.ESR['pulse_delay']``. 4. Perform read pulse ``ESRPulseSequence.ESR['read_pulse']``. 5. Repeat steps 2 and 3 for each ESR pulse in ``ESRPulseSequence.ESR['ESR_pulses']``, which by default contains single pulse ``ESRPulseSequence.ESR['ESR_pulse']``. 6. Perform empty-plunge-read sequence (EPR), but only if ``ESRPulseSequence.EPR['enabled']`` is True. EPR pulses are defined in ``ESRPulseSequence.EPR['pulses']``. 7. Perform any post_pulses defined in ``ESRPulseSequence.post_pulses``. Parameters: ESR (dict): Pulse settings for the ESR part of the pulse sequence. Contains the following items: * ``stage_pulse`` (Pulse): Stage pulse in which to perform ESR (e.g. plunge). Default is 'plunge `DCPulse`. * ``ESR_pulse`` (Pulse): Default ESR pulse to use. Default is 'ESR' ``SinePulse``. * ``ESR_pulses`` (List[Union[str, Pulse]]): List of ESR pulses to use. Can be strings, in which case the string should be an item in ``ESR`` whose value is a `Pulse`. * ``pulse_delay`` (float): ESR pulse delay after beginning of stage pulse. Default is 5 ms. * ``read_pulse`` (Pulse): Pulse after stage pulse for readout and initialization of electron. Default is 'read_initialize` `DCPulse`. EPR (dict): Pulse settings for the empty-plunge-read (EPR) part of the pulse sequence. This part is optional, and is used for non-ESR contast, and to measure dark counts and hence ESR contrast. Contains the following items: * ``enabled`` (bool): Enable EPR sequence. * ``pulses`` (List[Pulse]): List of pulses for EPR sequence. Default is ``empty``, ``plunge``, ``read_long`` `DCPulse`. pre_pulses (List[Pulse]): Pulses before main pulse sequence. Empty by default. post_pulses (List[Pulse]): Pulses after main pulse sequence. Empty by default. pulse_settings (dict): Dict containing all pulse settings. **kwargs: Additional kwargs to `PulseSequence`. Examples: The following code measures two ESR frequencies and performs an EPR from which the contrast can be determined for each ESR frequency: >>> ESR_pulse_sequence = ESRPulseSequence() >>> ESR_pulse_sequence.ESR['pulse_delay'] = 5e-3 >>> ESR_pulse_sequence.ESR['stage_pulse'] = DCPulse['plunge'] >>> ESR_pulse_sequence.ESR['ESR_pulse'] = FrequencyRampPulse('ESR_adiabatic') >>> ESR_pulse_sequence.ESR_frequencies = [39e9, 39.1e9] >>> ESR_pulse_sequence.EPR['enabled'] = True >>> ESR_pulse_sequence.pulse_sequence.generate() The total pulse sequence is plunge-read-plunge-read-empty-plunge-read with an ESR pulse in the first two plunge pulses, 5 ms after the start of the plunge pulse. The ESR pulses have different frequencies. Notes: For given pulse settings, `ESRPulseSequence.generate` will recreate the pulse sequence from settings. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.pulse_settings['pre_pulses'] = self.pre_pulses = [] self.pulse_settings['ESR'] = self.ESR = { 'ESR_pulse': SinePulse('ESR'), 'stage_pulse': DCPulse('plunge'), 'read_pulse': DCPulse('read_initialize', acquire=True), 'pre_delay': 5e-3, 'inter_delay': 5e-3, 'post_delay': 5e-3, 'ESR_pulses': ['ESR_pulse']} self.pulse_settings['EPR'] = self.EPR = { 'enabled': True, 'pulses':[ DCPulse('empty', acquire=True), DCPulse('plunge', acquire=True), DCPulse('read_long', acquire=True)]} self.pulse_settings['post_pulses'] = self.post_pulses = [DCPulse('final')] # Primary ESR pulses, the first ESR pulse in each plunge. # Used for assigning names during analysis self.primary_ESR_pulses = [] @property def ESR_frequencies(self): ESR_frequencies = [] for pulse in self.ESR['ESR_pulses']: if isinstance(pulse, Pulse): ESR_frequencies.append(pulse.frequency) elif isinstance(pulse, str): ESR_frequencies.append(self.ESR[pulse].frequency) elif isinstance(pulse, list): # Pulse is a list containing other pulses # These pulses will be joined in a single plunge ESR_subfrequencies = [] for subpulse in pulse: if isinstance(subpulse, Pulse): ESR_subfrequencies.append(subpulse.frequency) elif isinstance(subpulse, str): ESR_subfrequencies.append(self.ESR[subpulse].frequency) else: raise RuntimeError('ESR subpulse must be a pulse or' f'a string: {repr(subpulse)}') ESR_frequencies.append(ESR_subfrequencies) else: raise RuntimeError('ESR pulse must be Pulse, str, or list ' f'of pulses: {pulse}') return ESR_frequencies
[docs] def add_ESR_pulses(self, ESR_frequencies=None): """Add ESR pulses to the pulse sequence Args: ESR_frequencies: List of ESR frequencies. If provided, the pulses in ESR['ESR_pulses'] will be reset to copies of ESR['ESR_pulse'] with the provided frequencies Note: Each element in ESR_frequencies can also be a list of multiple frequencies, in which case multiple pulses with the provided subfrequencies will be used. """ # Manually set ESR frequencies if not explicitly provided, and a pulse # sequence has not yet been generated or the ``ESR['ESR_pulse']`` has # been modified if ESR_frequencies is None and \ (self._latest_pulse_settings is None or self.ESR['ESR_pulse'] != self._latest_pulse_settings['ESR']['ESR_pulse']): # Generate ESR frequencies via property ESR_frequencies = self.ESR_frequencies if ESR_frequencies is None and \ (self._latest_pulse_settings is None or self.ESR['ESR_pulse'] != self._latest_pulse_settings['ESR']['ESR_pulse']): # Generate ESR frequencies via property ESR_frequencies = self.ESR_frequencies if ESR_frequencies is not None: logger.warning("Resetting all ESR pulses to default ESR['ESR_pulse']") self.ESR['ESR_pulses'] = [] for ESR_frequency in ESR_frequencies: if isinstance(ESR_frequency, (float, int)): ESR_pulse = deepcopy(self.ESR['ESR_pulse']) ESR_pulse.frequency = ESR_frequency elif isinstance(ESR_frequency, list): ESR_pulse = [] for ESR_subfrequency in ESR_frequency: ESR_subpulse = deepcopy(self.ESR['ESR_pulse']) ESR_subpulse.frequency = ESR_subfrequency ESR_pulse.append(ESR_subpulse) else: raise RuntimeError('Each ESR frequency must be a number or a' f' list of numbers. {ESR_frequencies}') self.ESR['ESR_pulses'].append(ESR_pulse) # Convert any pulse strings to pulses if necessary for k, pulse in enumerate(self.ESR['ESR_pulses']): if isinstance(pulse, str): pulse_copy = deepcopy(self.ESR[pulse]) self.ESR['ESR_pulses'][k] = pulse_copy elif isinstance(pulse, list): # Pulse is a list containing other pulses # These pulses will be joined in a single plunge for kk, subpulse in enumerate(pulse): if isinstance(subpulse, str): subpulse_copy = deepcopy(self.ESR[subpulse]) self.ESR['ESR_pulses'][k][kk] = subpulse_copy self.primary_ESR_pulses = [] # Clear primary ESR pulses (first in each plunge) # Add pulses to pulse sequence for single_plunge_ESR_pulses in self.ESR['ESR_pulses']: # Each element should be the ESR pulses to apply within a single # plunge, between elements there is a read ESR_pulse = None if not isinstance(single_plunge_ESR_pulses, list): # Single ESR pulse provided, turn into list single_plunge_ESR_pulses = [single_plunge_ESR_pulses] plunge_pulse, = self.add(self.ESR['stage_pulse']) t_connect = partial(plunge_pulse['t_start'].connect, offset=self.ESR['pre_delay']) for k, ESR_subpulse in enumerate(single_plunge_ESR_pulses): # Add a plunge and read pulse for each frequency ESR_pulse, = self.add(ESR_subpulse) t_connect(ESR_pulse['t_start']) t_connect = partial(ESR_pulse['t_stop'].connect, offset=self.ESR['inter_delay']) if not k: self.primary_ESR_pulses.append(ESR_pulse) if ESR_pulse is not None: ESR_pulse['t_stop'].connect(plunge_pulse['t_stop'], offset=self.ESR['post_delay']) self.add(self.ESR['read_pulse'])
[docs] def generate(self, ESR_frequencies=None): self.clear() self.add(*self.pulse_settings['pre_pulses']) # Update self.ESR['ESR_pulses']. Converts any pulses that are strings to # actual pulses, and sets correct frequencies self.add_ESR_pulses(ESR_frequencies=ESR_frequencies) if self.EPR['enabled']: self._EPR_pulses = self.add(*self.EPR['pulses']) self._ESR_pulses = self.add(*self.post_pulses) self._latest_pulse_settings = deepcopy(self.pulse_settings)
[docs]class T2ElectronPulseSequence(PulseSequenceGenerator): """`PulseSequenceGenerator` for electron coherence (T2) measurements. This pulse sequence can handle measurements on the electron coherence time, including adding refocusing pulses In general the pulse sequence is as follows: 1. Perform any pre_pulses defined in ``T2ElectronPulseSequence.pre_pulses``. 2. Perform stage pulse ``T2ElectronPulseSequence.ESR['stage_pulse']``. By default, this is the ``plunge`` pulse. 3. Perform initial ESR pulse ``T2ElectronPulseSequence.ESR['ESR_initial_pulse']`` within plunge pulse, the delay until start of the ESR pulse is defined in ``T2ElectronPulseSequence.ESR['pre_delay']``. 4. For ``T2ElectronPulseSequence.ESR['num_refocusing_pulses']`` times, wait for ``T2ElectronPulseSequence.ESR['inter_delay']` and apply ``T2ElectronPulseSequence.ESR['ESR_refocusing_pulse]``. 5. Wait ``T2ElectronPulseSequence.ESR['ESR_refocusing_pulse']`` and apply ``T2ElectronPulseSequence.ESR['ESR_final_pulse']``. 6. Wait ``T2ElectronPulseSequence.ESR['post_delay']``, then stop stage pulse and Perform read pulse ``T2ElectronPulseSequence.ESR['read_pulse']``. 7. Perform empty-plunge-read sequence (EPR), but only if ``T2ElectronPulseSequence.EPR['enabled']`` is True. EPR pulses are defined in ``T2ElectronPulseSequence.EPR['pulses']``. 8. Perform any post_pulses defined in ``ESRPulseSequence.post_pulses``. Parameters: ESR (dict): Pulse settings for the ESR part of the pulse sequence. Contains the following items: :stage_pulse (Pulse): Stage pulse in which to perform ESR (e.g. plunge). Default is 'plunge `DCPulse`. :ESR_initial_pulse (Pulse): Initial ESR pulse to apply within stage pulse. Default is ``ESR_piHalf`` `SinePulse`. Ignored if set to ``None`` :ESR_refocusing_pulse (Pulse): Refocusing ESR pulses between initial and final ESR pulse. Zero refocusing pulses measures T2star, one refocusing pulse measures T2Echo. Default is ``ESR_pi`` `SinePulse`. :ESR_final_pulse (Pulse): Final ESR pulse within stage pulse. Default is ``ESR_piHalf`` `SinePulse`. Ignored if set to ``None``. :num_refocusing_pulses (int): Number of refocusing pulses ``T2ElectronPulseSequence.ESR['ESR_refocusing_pulse']`` to apply. :pre_delay (float): Delay after stage pulse before first ESR pulse. :inter_delay (float): Delay between successive ESR pulses. :post_delay (float): Delay after final ESR pulse. :read_pulse (Pulse): Pulse after stage pulse for readout and initialization of electron. Default is 'read_initialize` `DCPulse`. EPR (dict): Pulse settings for the empty-plunge-read (EPR) part of the pulse sequence. This part is optional, and is used for non-ESR contast, and to measure dark counts and hence ESR contrast. Contains the following items: :enabled: (bool): Enable EPR sequence. :pulses: (List[Pulse]): List of pulses for EPR sequence. Default is ``empty``, ``plunge``, ``read_long`` `DCPulse`. pre_pulses (List[Pulse]): Pulses before main pulse sequence. Empty by default. post_pulses (List[Pulse]): Pulses after main pulse sequence. Empty by default. pulse_settings (dict): Dict containing all pulse settings. **kwargs: Additional kwargs to `PulseSequence`. Notes: For given pulse settings, `T2ElectronPulseSequence.generate` will recreate the pulse sequence from settings. """ def __init__(self, **kwargs): super().__init__(pulses=[], **kwargs) self.pulse_settings['ESR'] = self.ESR = { 'stage_pulse': DCPulse('plunge'), 'ESR_initial_pulse': SinePulse('ESR_PiHalf'), 'ESR_refocusing_pulse': SinePulse('ESR_Pi'), 'ESR_final_pulse': SinePulse('ESR_PiHalf'), 'read_pulse': DCPulse('read'), 'num_refocusing_pulses': 0, 'pre_delay': None, 'inter_delay': None, 'post_delay': None, } self.pulse_settings['EPR'] = self.EPR = { 'enabled': True, 'pulses':[ DCPulse('empty', acquire=True), DCPulse('plunge', acquire=True), DCPulse('read_long', acquire=True)]} self.pulse_settings['pre_pulses'] = self.pre_pulses = [] self.pulse_settings['post_pulses'] = self.post_pulses = []
[docs] def add_ESR_pulses(self): # Add stage pulse, duration will be specified later stage_pulse, = self.add(self.ESR['stage_pulse']) t = stage_pulse.t_start + self.ESR['pre_delay'] # Add initial pulse (evolve to state where T2 effects can be observed) if self.ESR['ESR_initial_pulse'] is not None: ESR_initial_pulse, = self.add(self.ESR['ESR_initial_pulse']) ESR_initial_pulse.t_start = t t += ESR_initial_pulse.duration for k in range(self.ESR['num_refocusing_pulses']): t += self.ESR['inter_delay'] ESR_refocusing_pulse = self.add(self.ESR['ESR_refocusing_pulse']) ESR_refocusing_pulse.t_start = t t += ESR_refocusing_pulse.duration t += self.ESR['inter_delay'] if self.ESR['ESR_final_pulse'] is not None: ESR_final_pulse, = self.add(self.ESR['ESR_final_pulse']) ESR_final_pulse.t_start = t t += ESR_final_pulse.duration t += self.ESR['post_delay'] stage_pulse.duration = t - stage_pulse.t_start # Add final read pulse self.add(self.ESR['read_pulse'])
[docs] def generate(self): """ Updates the pulse sequence """ # Initialize pulse sequence self.clear() self.add(*self.pulse_settings['pre_pulses']) self.add_ESR_pulses() if self.EPR['enabled']: self.add(*self.EPR['pulses']) self.add(*self.pulse_settings['post_pulses']) # Create copy of current pulse settings for comparison later self._latest_pulse_settings = deepcopy(self.pulse_settings)
[docs]class NMRPulseSequence(PulseSequenceGenerator): """`PulseSequenceGenerator` for nuclear magnetic resonance (NMR). This pulse sequence can handle many of the basic pulse sequences involving NMR. The pulse sequence is generated from its pulse settings attributes. In general, the pulse sequence is as follows: 1. Perform any pre_pulses defined in ``NMRPulseSequence.pre_pulses``. 2. Perform NMR sequence 1. Perform stage pulse ``NMRPulseSequence.NMR['stage_pulse']``. Default is 'empty' `DCPulse`. 2. Perform NMR pulses within the stage pulse. The NMR pulses defined in ``NMRPulseSequence.NMR['NMR_pulses']`` are applied successively. The delay after start of the stage pulse is ``NMRPulseSequence.NMR['pre_delay']``, delays between NMR pulses is ``NMRPulseSequence.NMR['inter_delay']``, and the delay after the final NMR pulse is ``NMRPulseSequence.NMR['post_delay']``. 3. Perform ESR sequence 1. Perform stage pulse ``NMRPulseSequence.ESR['stage_pulse']``. Default is 'plunge' `DCPulse`. 2. Perform ESR pulse within stage pulse for first pulse in ``NMRPulseSequence.ESR['ESR_pulses']``. 3. Perform ``NMRPulseSequence.ESR['read_pulse']``, and acquire trace. 4. Repeat steps 1 - 3 for each ESR pulse. The different ESR pulses usually correspond to different ESR frequencies (see `NMRPulseSequence`.ESR_frequencies). 5. Repeat steps 1 - 4 for ``NMRPulseSequence.ESR['shots_per_frequency']`` This effectively interleaves the ESR pulses, which counters effects of the nucleus flipping within an acquisition. By measuring the average up proportion for each ESR frequency, a switching between high and low up proportion indicates a flipping of the nucleus Parameters: NMR (dict): Pulse settings for the NMR part of the pulse sequence. Contains the following items: * ``stage_pulse`` (Pulse): Stage pulse in which to perform NMR (e.g. plunge). Default is 'empty' `DCPulse`. Duration of stage pulse is adapted to NMR pulses and delays. * ``NMR_pulse`` (Pulse): Default NMR pulse to use. By default 'NMR' `SinePulse`. * ``NMR_pulses`` (List[Union[str, Pulse]]): List of NMR pulses to successively apply. Can be strings, in which case the string should be an item in ``NMR`` whose value is a `Pulse`. Default is single element ``NMRPulseSequence.NMR['NMR_pulse']``. * ``pre_delay`` (float): Delay after start of ``stage`` pulse, until first NMR pulse. * ``inter_delay`` (float): Delay between successive NMR pulses. * ``post_delay`` (float): Delay after final NMR pulse until stage pulse end. ESR (dict): Pulse settings for the ESR part of the pulse sequence. Contains the following items: * ``stage_pulse`` (Pulse): Stage pulse in which to perform ESR (e.g. plunge). Default is 'plunge `DCPulse`. * ``ESR_pulse`` (Pulse): Default ESR pulse to use. Default is 'ESR' ``SinePulse``. * ``ESR_pulses`` (List[Union[str, Pulse]]): List of ESR pulses to use. Can be strings, in which case the string should be an item in ``ESR`` whose value is a `Pulse`. * ``pulse_delay`` (float): ESR pulse delay after beginning of stage pulse. Default is 5 ms. * ``read_pulse`` (Pulse): Pulse after stage pulse for readout and initialization of electron. Default is 'read_initialize` `DCPulse`. EPR (dict): Pulse settings for the empty-plunge-read (EPR) part of the pulse sequence. This part is optional, and is used for non-ESR contast, and to measure dark counts and hence ESR contrast. Contains the following items: * ``enabled`` (bool): Enable EPR sequence. * ``pulses`` (List[Pulse]): List of pulses for EPR sequence. Default is ``empty``, ``plunge``, ``read_long`` `DCPulse`. pre_pulses (List[Pulse]): Pulses before main pulse sequence. Empty by default. pre_ESR_pulses (List[Pulse]): Pulses before ESR readout pulse sequence. Empty by default. post_pulses (List[Pulse]): Pulses after main pulse sequence. Empty by default. pulse_settings (dict): Dict containing all pulse settings. **kwargs: Additional kwargs to `PulseSequence`. See Also: NMRParameter Notes: For given pulse settings, `NMRPulseSequence.generate` will recreate the pulse sequence from settings. """ def __init__(self, pulses=[], **kwargs): super().__init__(pulses=pulses, **kwargs) self.pulse_settings['NMR'] = self.NMR = { 'stage_pulse': DCPulse('empty'), 'NMR_pulse': SinePulse('NMR'), 'NMR_pulses': ['NMR_pulse'], 'intermediate_pulses' : [], 'pre_delay': 5e-3, 'inter_delay': 1e-3, 'post_delay': 2e-3} self.pulse_settings['ESR'] = self.ESR = { 'ESR_pulse': FrequencyRampPulse('adiabatic_ESR'), 'ESR_pulses': ['ESR_pulse'], 'stage_pulse': DCPulse('plunge'), 'read_pulse': DCPulse('read_initialize', acquire=True), 'pre_delay': 5e-3, 'post_delay': 5e-3, 'inter_delay': 1e-3, 'shots_per_frequency': 25} self.pulse_settings['pre_pulses'] = self.pre_pulses = [] self.pulse_settings['pre_ESR_pulses'] = self.pre_ESR_pulses = [] self.pulse_settings['post_pulses'] = self.post_pulses = [] self.generate()
[docs] def add_NMR_pulses(self, pulse_sequence=None): if pulse_sequence is None: pulse_sequence = self # Convert any pulse strings to pulses if necessary for k, pulse in enumerate(self.NMR['NMR_pulses']): if isinstance(pulse, str): pulse_copy = deepcopy(self.NMR[pulse]) self.NMR['NMR_pulses'][k] = pulse_copy elif isinstance(pulse, Iterable): # Pulse is a list containing sub-pulses # These pulses will be sequenced during a single stage pulse for kk, subpulse in enumerate(pulse): if isinstance(subpulse, str): subpulse_copy = deepcopy(self.NMR[subpulse]) self.NMR['NMR_pulses'][k][kk] = subpulse_copy self.primary_NMR_pulses = [] # Clear primary NMR pulses (first in each stage pulse) # Add pulses to pulse sequence for k, single_stage_NMR_pulses in enumerate(self.NMR['NMR_pulses']): # Each element should be the NMR pulses to apply within a single # stage, between each subsequence there will be a pre-delay and # post-delay if not isinstance(single_stage_NMR_pulses, Iterable): # Single NMR pulse provided, turn into list single_stage_NMR_pulses = [single_stage_NMR_pulses] NMR_stage_pulse, = self.add(self.NMR['stage_pulse']) t_connect = partial(NMR_stage_pulse['t_start'].connect, offset=self.NMR['pre_delay']) self.primary_NMR_pulses.append(single_stage_NMR_pulses[0]) for NMR_subpulse in single_stage_NMR_pulses: NMR_pulse, = self.add(NMR_subpulse) t_connect(NMR_pulse['t_start']) t_connect = partial(NMR_pulse['t_stop'].connect, offset=self.NMR['inter_delay']) if NMR_pulse is not None: NMR_pulse['t_stop'].connect(NMR_stage_pulse['t_stop'], offset=self.NMR['post_delay']) if k < len(self.NMR['NMR_pulses'])-1: # Add any intermediate pulses, except for the final NMR sequence t_connect = partial(NMR_stage_pulse['t_stop'].connect, offset=0) for intermediate_pulse in self.NMR['intermediate_pulses']: int_pulse, = self.add(intermediate_pulse) t_connect(int_pulse['t_start']) t_connect = partial(int_pulse['t_stop'].connect, offset=0) return pulse_sequence
[docs] def add_ESR_pulses(self, pulse_sequence=None, previous_pulse=None): if pulse_sequence is None: pulse_sequence = self for _ in range(self.ESR['shots_per_frequency']): for ESR_pulses in self.ESR['ESR_pulses']: # Add a plunge and read pulse for each frequency if not isinstance(ESR_pulses, list): # Treat frequency as list, as one could add multiple ESR # pulses ESR_pulses = [ESR_pulses] stage_pulse, = pulse_sequence.add(self.ESR['stage_pulse']) for k, ESR_pulse in enumerate(ESR_pulses): if isinstance(ESR_pulse, str): # Pulse is a reference to some pulse in self.ESR ESR_pulse = self.ESR[ESR_pulse] ESR_pulse, = pulse_sequence.add(ESR_pulse) # Delay also depends on any previous ESR pulses delay = self.ESR['pre_delay'] + k * self.ESR['inter_delay'] stage_pulse['t_start'].connect(ESR_pulse['t_start'], offset=delay) ESR_pulse['duration'].connect(stage_pulse['t_stop'], offset=lambda p: p.parent.t_start + self.ESR['post_delay']) pulse_sequence.add(self.ESR['read_pulse'])
[docs] def generate(self): """Updates the pulse sequence""" # Initialize pulse sequence self.clear() self.add(*self.pulse_settings['pre_pulses']) self.add_NMR_pulses() # Note: This was added when performing NMR on the electron-up manifold, # where it is important to reload a spin-down electron for readout. self.add(*self.pulse_settings['pre_ESR_pulses']) self.add_ESR_pulses() self.add(*self.pulse_settings['post_pulses']) # Create copy of current pulse settings for comparison later self._latest_pulse_settings = deepcopy(self.pulse_settings)
[docs]class NMRCPMGPulseSequence(NMRPulseSequence): """`PulseSequenceGenerator` for nuclear magnetic resonance (NMR). This pulse sequence can handle many of the basic pulse sequences involving NMR. The pulse sequence is generated from its pulse settings attributes. In general, the pulse sequence is as follows: 1. Perform any pre_pulses defined in ``NMRPulseSequence.pre_pulses``. 2. Perform NMR sequence 1. Perform stage pulse ``NMRPulseSequence.NMR['stage_pulse']``. Default is 'empty' `DCPulse`. 2. Perform NMR pulses within the stage pulse. The NMR pulses defined in ``NMRPulseSequence.NMR['NMR_pulses']`` are applied successively. The delay after start of the stage pulse is ``NMRPulseSequence.NMR['pre_delay']``, delays between NMR pulses is ``NMRPulseSequence.NMR['inter_delay']``, and the delay after the final NMR pulse is ``NMRPulseSequence.NMR['post_delay']``. 3. Perform ESR sequence 1. Perform stage pulse ``NMRPulseSequence.ESR['stage_pulse']``. Default is 'plunge' `DCPulse`. 2. Perform ESR pulse within stage pulse for first pulse in ``NMRPulseSequence.ESR['ESR_pulses']``. 3. Perform ``NMRPulseSequence.ESR['read_pulse']``, and acquire trace. 4. Repeat steps 1 - 3 for each ESR pulse. The different ESR pulses usually correspond to different ESR frequencies (see `NMRPulseSequence`.ESR_frequencies). 5. Repeat steps 1 - 4 for ``NMRPulseSequence.ESR['shots_per_frequency']`` This effectively interleaves the ESR pulses, which counters effects of the nucleus flipping within an acquisition. By measuring the average up proportion for each ESR frequency, a switching between high and low up proportion indicates a flipping of the nucleus Parameters: NMR (dict): Pulse settings for the NMR part of the pulse sequence. Contains the following items: * ``stage_pulse`` (Pulse): Stage pulse in which to perform NMR (e.g. plunge). Default is 'empty' `DCPulse`. Duration of stage pulse is adapted to NMR pulses and delays. * ``NMR_pulse`` (Pulse): Default NMR pulse to use. By default 'NMR' `SinePulse`. * ``NMR_pulses`` (List[Union[str, Pulse]]): List of NMR pulses to successively apply. Can be strings, in which case the string should be an item in ``NMR`` whose value is a `Pulse`. Default is single element ``NMRPulseSequence.NMR['NMR_pulse']``. * ``pre_delay`` (float): Delay after start of ``stage`` pulse, until first NMR pulse. * ``inter_delay`` (float): Delay between successive NMR pulses. * ``post_delay`` (float): Delay after final NMR pulse until stage pulse end. ESR (dict): Pulse settings for the ESR part of the pulse sequence. Contains the following items: * ``stage_pulse`` (Pulse): Stage pulse in which to perform ESR (e.g. plunge). Default is 'plunge `DCPulse`. * ``ESR_pulse`` (Pulse): Default ESR pulse to use. Default is 'ESR' ``SinePulse``. * ``ESR_pulses`` (List[Union[str, Pulse]]): List of ESR pulses to use. Can be strings, in which case the string should be an item in ``ESR`` whose value is a `Pulse`. * ``pulse_delay`` (float): ESR pulse delay after beginning of stage pulse. Default is 5 ms. * ``read_pulse`` (Pulse): Pulse after stage pulse for readout and initialization of electron. Default is 'read_initialize` `DCPulse`. EPR (dict): Pulse settings for the empty-plunge-read (EPR) part of the pulse sequence. This part is optional, and is used for non-ESR contast, and to measure dark counts and hence ESR contrast. Contains the following items: * ``enabled`` (bool): Enable EPR sequence. * ``pulses`` (List[Pulse]): List of pulses for EPR sequence. Default is ``empty``, ``plunge``, ``read_long`` `DCPulse`. pre_pulses (List[Pulse]): Pulses before main pulse sequence. Empty by default. pre_ESR_pulses (List[Pulse]): Pulses before ESR readout pulse sequence. Empty by default. post_pulses (List[Pulse]): Pulses after main pulse sequence. Empty by default. pulse_settings (dict): Dict containing all pulse settings. **kwargs: Additional kwargs to `PulseSequence`. See Also: NMRParameter Notes: For given pulse settings, `NMRPulseSequence.generate` will recreate the pulse sequence from settings. """ def __init__(self, pulses=[], **kwargs): super().__init__(pulses=pulses, **kwargs) self.pulse_settings['NMR'] = self.NMR = { 'stage_pulse': DCPulse('empty'), 'NMR_pulse': SinePulse('NMR'), 'NMR_pulses': ['NMR_pulse'], 'pre_delay': 5e-3, 'inter_delay': 1e-3, 'post_delay': 2e-3} self.pulse_settings['ESR'] = self.ESR = { 'ESR_pulse': FrequencyRampPulse('adiabatic_ESR'), 'ESR_pulses': ['ESR_pulse'], 'stage_pulse': DCPulse('plunge'), 'read_pulse': DCPulse('read_initialize', acquire=True), 'pre_delay': 5e-3, 'post_delay': 5e-3, 'inter_delay': 1e-3, 'shots_per_frequency': 25} self.pulse_settings['pre_pulses'] = self.pre_pulses = [] self.pulse_settings['pre_ESR_pulses'] = self.pre_ESR_pulses = [] self.pulse_settings['post_pulses'] = self.post_pulses = [] self.generate()
[docs] def add_NMR_pulses(self, pulse_sequence=None): if pulse_sequence is None: pulse_sequence = self # Convert any pulse strings to pulses if necessary for k, pulse in enumerate(self.NMR['NMR_pulses']): if isinstance(pulse, str): pulse_copy = deepcopy(self.NMR[pulse]) self.NMR['NMR_pulses'][k] = pulse_copy elif isinstance(pulse, Iterable): # Pulse is a list containing sub-pulses # These pulses will be sequenced during a single stage pulse for kk, subpulse in enumerate(pulse): if isinstance(subpulse, str): subpulse_copy = deepcopy(self.NMR[subpulse]) self.NMR['NMR_pulses'][k][kk] = subpulse_copy self.primary_NMR_pulses = [] # Clear primary NMR pulses (first in each stage pulse) # Add pulses to pulse sequence for single_stage_NMR_pulses in self.NMR['NMR_pulses']: # Each element should be the NMR pulses to apply within a single # stage, between each subsequence there will be a pre-delay and # post-delay if not isinstance(single_stage_NMR_pulses, Iterable): # Single NMR pulse provided, turn into list single_stage_NMR_pulses = [single_stage_NMR_pulses] NMR_stage_pulse, = self.add(self.NMR['stage_pulse']) t_connect = partial(NMR_stage_pulse['t_start'].connect, offset=self.NMR['pre_delay']) self.primary_NMR_pulses.append(single_stage_NMR_pulses[0]) for k, NMR_subpulse in enumerate(single_stage_NMR_pulses): NMR_pulse, = self.add(NMR_subpulse) t_connect(NMR_pulse['t_start']) # The first and last pulses have only a single inter-delay time between pulses if k == 0 or k == len(single_stage_NMR_pulses) - 2: t_connect = partial(NMR_pulse['t_stop'].connect, offset=self.NMR['inter_delay']) else: t_connect = partial(NMR_pulse['t_stop'].connect, offset=self.NMR['inter_delay']*2) if NMR_pulse is not None: NMR_pulse['t_stop'].connect(NMR_stage_pulse['t_stop'], offset=self.NMR['post_delay']) return pulse_sequence
[docs] def add_ESR_pulses(self, pulse_sequence=None, previous_pulse=None): if pulse_sequence is None: pulse_sequence = self for _ in range(self.ESR['shots_per_frequency']): for ESR_pulses in self.ESR['ESR_pulses']: # Add a plunge and read pulse for each frequency if not isinstance(ESR_pulses, list): # Treat frequency as list, as one could add multiple ESR # pulses ESR_pulses = [ESR_pulses] stage_pulse, = pulse_sequence.add(self.ESR['stage_pulse']) delay = self.ESR['pre_delay'] for k, ESR_pulse in enumerate(ESR_pulses): if isinstance(ESR_pulse, str): # Pulse is a reference to some pulse in self.ESR ESR_pulse = self.ESR[ESR_pulse] ESR_pulse, = pulse_sequence.add(ESR_pulse) # Delay also depends on any previous ESR pulses stage_pulse['t_start'].connect(ESR_pulse['t_start'], offset=delay) delay = delay + ESR_pulse['duration'].get() + self.ESR['inter_delay'] ESR_pulse['duration'].connect(stage_pulse['t_stop'], offset=lambda p: p.parent.t_start + self.ESR['post_delay']) pulse_sequence.add(self.ESR['read_pulse'])
[docs] def generate(self): """Updates the pulse sequence""" # Initialize pulse sequence self.clear() self.add(*self.pulse_settings['pre_pulses']) self.add_NMR_pulses() # Note: This was added when performing NMR on the electron-up manifold, # where it is important to reload a spin-down electron for readout. self.add(*self.pulse_settings['pre_ESR_pulses']) self.add_ESR_pulses() self.add(*self.pulse_settings['post_pulses']) # Create copy of current pulse settings for comparison later self._latest_pulse_settings = deepcopy(self.pulse_settings)
[docs]class FlipFlopPulseSequence(PulseSequenceGenerator): """`PulseSequenceGenerator` for hitting the flip-flop transition The flip-flop transition is the one where both the electron and nucleus flip in opposite direction, thus keeping the total spin constant. This pulse sequence is mainly used to flip the nucleus to a certain state without having to perform NMR or even having to measure the electron. The flip-flop transitions between a nuclear spin state S1 and (S1+1) is: f_ESR(S1) + A/2 + gamma_n * B_0, where f_ESR(S1) is the ESR frequency for nuclear state S1, A is the hyperfine, gamma_n is the nuclear Zeeman, and B_0 is the static magnetic field. The transition will flip (electron down, nucleus S+1) to (electron up, nucleus S) and vice versa. Parameters: ESR (dict): Pulse settings for the ESR part of the pulse sequence. Contains the following items: * ``frequency`` (float): ESR frequency below the flip-flop transition (Hz). * ``hyperfine`` (float): Hyperfine interaction (Hz). * ``nuclear_zeeman`` (float): Nuclear zeeman strength (gamma_n*B_0) * ``stage_pulse`` (Pulse): Stage pulse in which to perform ESR (e.g. plunge). Default is `DCPulse`('plunge'). * ``pre_flip_ESR_pulse`` (Pulse): ESR pulse to use before the flip-flop pulse to pre-flip the electron to spin-up, which allows the nucleus to be flipped to a higher state. Default is `SinePulse`('ESR'). * ``flip_flop_pulse`` (Pulse): Flip-flop ESR pulse, whose frequency will be set to A/2 + gamma_n*B_0 higher than the ``frequency`` setting. Default pulse is `SinePulse`('ESR') * ``pre_flip`` (bool): Whether to pre-flip the electron, to transition to a higher nuclear state. Default is False. * ``pre_delay`` (float): Delay between start of stage pulse and first pulse (``pre_flip_ESR_pulse`` or ``flip_flop_pulse``). * ``inter_delay`` (float): Delay between ``pre_flip_ESR_pulse`` and ``flip_flop_pulse``. Ignored if pre_flip is False * ``post_delay`` (float): Delay after last frequency pulse and end of stage pulse. pre_pulses (List[Pulse]): Pulses before main pulse sequence. Empty by default. post_pulses (List[Pulse]): Pulses after main pulse sequence. Empty by default. pulse_settings (dict): Dict containing all pulse settings. **kwargs: Additional kwargs to `PulseSequence`. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.pulse_settings['ESR'] = self.ESR = { 'frequencies': [28e9, 28e9], 'hyperfine': None, 'nuclear_zeeman': -5.5e6, 'stage_pulse': DCPulse('plunge', acquire=True), 'pre_flip_ESR_pulse': SinePulse('ESR'), 'flip_flop_pulse': SinePulse('ESR'), 'pre_flip': False, 'pre_delay': 5e-3, 'inter_delay': 1e-3, 'post_delay': 5e-3} self.pulse_settings['pre_pulses'] = self.pre_pulses = [] self.pulse_settings['post_pulses'] = self.post_pulses = [DCPulse('read')] self.generate()
[docs] def add_ESR_pulses(self): stage_pulse, = self.add(self.ESR['stage_pulse']) ESR_t_start = partial(stage_pulse['t_start'].connect, offset=self.ESR['pre_delay']) if self.ESR['pre_flip']: # First add the pre-flip the ESR pulses (start with excited electron) for ESR_frequency in self.ESR['frequencies']: pre_flip_ESR_pulse, = self.add(self.ESR['pre_flip_ESR_pulse']) pre_flip_ESR_pulse.frequency = ESR_frequency ESR_t_start(pre_flip_ESR_pulse['t_start']) # Update t_start of next ESR pulse ESR_t_start = partial(pre_flip_ESR_pulse['t_stop'].connect, offset=self.ESR['inter_delay']) flip_flop_ESR_pulse, = self.add(self.ESR['flip_flop_pulse']) ESR_t_start(flip_flop_ESR_pulse['t_start']) # Calculate flip-flop frequency ESR_max_frequency = np.max(self.ESR['frequencies']) hyperfine = self.ESR['hyperfine'] if hyperfine is None: # Choose difference between two ESR frequencies hyperfine = float(np.abs(np.diff(self.ESR['frequencies']))) flip_flop_ESR_pulse.frequency = (ESR_max_frequency - hyperfine / 2 - self.ESR['nuclear_zeeman']) flip_flop_ESR_pulse['t_stop'].connect(stage_pulse['t_stop'], offset=self.ESR['post_delay'])
[docs] def generate(self): """Updates the pulse sequence""" self.clear() self.add(*self.pulse_settings['pre_pulses']) self.add_ESR_pulses() self.add(*self.pulse_settings['post_pulses'])
[docs]class ESRRamseyDetuningPulseSequence(ESRPulseSequence): """" Created to implement an arbitrary number of DC pulses in a Ramsey sequence during the wait time. Please Refer to ESRPulseSequence for the ESR pulses. Highlights: - DC pulses can be stored in ['ESR']['detuning_pulses'] and will become the new 'stage_pulse' - t_start_detuning is the time at which the DC detuning pulses start. In the case the detuning starts right after the ESR pi/2 , then this time should be equal to ['pre_delay'] +ESR['piHalf'].duration - If the time for the detuning pulses is shorter that the total stage duration, the final part of the pulse (called post_stage) will the standard stage pulse """ def __init__(self, **kwargs): super().__init__(**kwargs) self.pulse_settings['ESR']['t_start_detuning'] = 0 self.pulse_settings['ESR']['detuning_pulses'] = []
[docs] def add_ESR_pulses(self, ESR_frequencies=None): super().add_ESR_pulses(ESR_frequencies=ESR_frequencies) # At this point there is a single `stage` pulse for each group of ESR pulses. # We want to inject our detuning pulses in between this stage pulse if self.pulse_settings['EPR']['enabled'] : raise NotImplementedError('Currently not programmed to include EPR pulse') stage_pulse = self.get_pulse(name=self.ESR['stage_pulse'].name) assert stage_pulse is not None, "Could not find existing stage pulse in pulse sequence" self.remove(stage_pulse) if any(pulse.connection_label != stage_pulse.connection_label or pulse.connection != stage_pulse.connection for pulse in self.ESR['detuning_pulses']): raise RuntimeError('All detuning pulses must have same connection as stage pulse') t = stage_pulse._delay + self.ESR['t_start_detuning'] # Add an initial stage pulse if t_start_detuning > 0 if self.pulse_settings['ESR']['t_start_detuning'] > 0: pre_stage_pulse, = self.add(stage_pulse) pre_stage_pulse.name = 'pre_stage' pre_stage_pulse.t_stop = t # Add all detuning pulses for pulse in self.ESR['detuning_pulses']: detuning_pulse, = self.add(pulse) detuning_pulse.t_start = t t += detuning_pulse.duration if t > stage_pulse.t_stop: raise RuntimeError('Total duration of detuning pulses exceeds total stage pulse duration') elif t < stage_pulse.t_stop: # Add a final stage pulse post_stage_pulse, = self.add(stage_pulse) post_stage_pulse.name = 'post_stage' post_stage_pulse.t_start = t post_stage_pulse.t_stop = stage_pulse.t_stop