diff --git a/.gitignore b/.gitignore index cfde3773c..e46fa3589 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ experiments_folder.txt _modules */make.bat Qcodes + +venv/ diff --git a/silq/parameters/acquisition_parameters.py b/silq/parameters/acquisition_parameters.py index b18e06c11..6c91f7935 100644 --- a/silq/parameters/acquisition_parameters.py +++ b/silq/parameters/acquisition_parameters.py @@ -15,7 +15,7 @@ from silq import config from silq.pulses import * from silq.pulses.pulse_sequences import ESRPulseSequence, NMRPulseSequence, \ - T2ElectronPulseSequence, FlipFlopPulseSequence, ESRRamseyDetuningPulseSequence + T2ElectronPulseSequence, FlipFlopPulseSequence, ESRRamseyDetuningPulseSequence, NMRCPMGPulseSequence from silq.analysis import analysis from silq.tools.general_tools import SettingsClass, clear_single_settings, \ attribute_from_config, UpdateDotDict, convert_setpoints, \ @@ -23,8 +23,8 @@ __all__ = ['AcquisitionParameter', 'DCParameter', 'TraceParameter', 'DCSweepParameter', 'EPRParameter', 'ESRParameter', - 'NMRParameter', 'EDSRParameter', 'VariableReadParameter', 'BlipsParameter', - 'FlipNucleusParameter', 'FlipFlopParameter', 'NeuralNetworkParameter', + 'NMRParameter', 'EDSRParameter', 'VariableReadParameter', 'BlipsParameter', 'T2ElectronParameter', + 'NMRCPMGParameter','FlipNucleusParameter', 'FlipFlopParameter', 'NeuralNetworkParameter', 'NeuralRetuneParameter','ESRRamseyDetuningParameter'] logger = logging.getLogger(__name__) @@ -1948,6 +1948,7 @@ def analyse(self, traces: Dict[str, Dict[str, np.ndarray]] = None): up_proportions = np.zeros((len(self.ESR_frequencies), self.samples)) state_probability = np.zeros(len(self.ESR_frequencies)) threshold_up_proportion = np.zeros(len(self.ESR_frequencies)) + for f_idx, ESR_frequency in enumerate(self.ESR_frequencies): for sample in range(self.samples): # Create array containing all read traces @@ -1968,6 +1969,7 @@ def analyse(self, traces: Dict[str, Dict[str, np.ndarray]] = None): up_proportions[f_idx, sample] = read_result['up_proportion'] results['results_read'].append(read_result) + if self.threshold_up_proportion is None: threshold_up_proportion[f_idx] = analysis.determine_threshold_up_proportion_single_state( up_proportions_arr=up_proportions[f_idx], @@ -2659,3 +2661,313 @@ def analyse(self, traces=None, plot=False): self.results = results return results + +class NMRCPMGParameter(AcquisitionParameter): + """ Parameter for most measurements involving an NMR pulse. + + This parameter can apply several NMR pulses, and also measure several ESR + frequencies. It uses the `NMRPulseSequence`, which will generate a pulse + sequence from settings (see parameters below). + + Refer to NMRCPMGPulse sequence to learn about the pulse sequence performed + by this parameter + + The acquisition for this parameter is repeated ``NMRCPMGParameter.samples`` times. If the nucleus + is in one of the states for which an ESR frequency is on resonance, a high + ``up_proportion`` is measured, while for the other frequencies a low + ``up_proportion`` is measured. By looking over successive samples and + measuring how often the ``up_proportions`` switch between above/below + ``NMRParameter.threshold_up_proportion``, nuclear flips can be measured + (see `NMRParameter.analyse` and `analyse_flips_old`). + + Args: + name: Parameter name + **kwargs: Additional kwargs passed to `AcquisitionParameter` + + Parameters: + NMR (dict): `NMRCPMGPulseSequence` pulse settings for NMR. Settings are: + ``stage_pulse``, ``NMR_pulse``, ``NMR_pulses``, ``pre_delay``, + ``inter_delay``, ``post_delay``. + ESR (dict): `NMRCPMGPulseSequence` pulse settings for ESR. Settings are: + ``ESR_pulse``, ``stage_pulse``, ``ESR_pulses``, ``read_pulse``, + ``pulse_delay``. + EPR (dict): `PulseSequenceGenerator` settings for EPR. This is optional + and can be toggled in ``EPR['enabled']``. If disabled, contrast is + not calculated. + pre_pulses (List[Pulse]): Pulses to place at the start of the sequence. + post_pulses (List[Pulse]): Pulses to place at the end of the sequence. + pulse_sequence (PulseSequence): Pulse sequence used for acquisition. + ESR_frequencies (List[float]): List of ESR frequencies to use. When set, + a copy of ``NMRCPMGPulseSequence.ESR['ESR_pulse']`` is created for each + frequency, and added to ``NMRCPMGPulseSequence.ESR['ESR_pulses']``. + samples (int): Number of acquisition samples + results (dict): Results obtained after analysis of traces. + t_skip (float): initial part of read trace to ignore for measuring + blips. Useful if there is a voltage spike at the start, which could + otherwise be measured as a ``blip``. Retrieved from + ``silq.config.properties.t_skip``. + t_read (float): duration of read trace to include for measuring blips. + Useful if latter half of read pulse is used for initialization. + Retrieved from ``silq.config.properties.t_read``. + threshold_up_proportion (Union[float, Tuple[float, float]): threshold + for up proportions needed to determine ESR pulse to be on-resonance. + If tuple, first element is threshold below which ESR pulse is + off-resonant, and second element is threshold above which ESR pulse + is on-resonant. Useful for filtering of up proportions at boundary. + Retrieved from + ``silq.config.properties.threshold_up_proportion``. + traces (dict): Acquisition traces segmented by pulse and acquisition + label + silent (bool): Print results after acquisition + continuous (bool): If True, instruments keep running after acquisition. + Useful if stopping/starting instruments takes a considerable amount + of time. + properties_attrs (List[str]): Attributes to match with + ``silq.config.properties`` See notes below for more info. + save_traces (bool): Save acquired traces to disk. + If the acquisition has been part of a measurement, the traces are + stored in a subfolder of the corresponding data set. + Otherwise, a new dataset is created. + dataset (DataSet): Traces DataSet + base_folder (str): Base folder in which to save traces. If not specified, + and acquisition is part of a measurement, the base folder is the + folder of the measurement data set. Otherwise, the base folder is + the default data folder + subfolder (str): Subfolder within the base folder to save traces. + + Note: + - The `NMRCPMGPulseSequence` does not have an empty-plunge-read (EPR) + sequence, and therefore does not add a contrast or dark counts. + Verifying that the system is in tune is therefore a little bit tricky. + + """ + def __init__(self, name: str = 'NMR', + names: List[str] = ['flips', 'flip_probability', + 'up_proportions', 'state_probability', + 'threshold_up_proportion'], + **kwargs): + """ + Parameter used to determine the Rabi frequency + """ + self.pulse_sequence = NMRCPMGPulseSequence() + self.NMR = self.pulse_sequence.NMR + self.ESR = self.pulse_sequence.ESR + self.pre_pulses = self.pulse_sequence.pulse_settings['pre_pulses'] + self.pre_ESR_pulses = self.pulse_sequence.pulse_settings['pre_ESR_pulses'] + self.post_pulses = self.pulse_sequence.pulse_settings['post_pulses'] + + super().__init__(name=name, + names=names, + snapshot_value=False, + properties_attrs=['t_read', 't_skip', + 'threshold_up_proportion'], + **kwargs) + + @property + def names(self): + names = [] + + for name in self._names: + if name in ['flips', 'flip_probability', + 'up_proportions', 'state_probability', + 'threshold_up_proportion']: + if len(self.ESR_frequencies) == 1: + names.append(name) + else: + names += [f'{name}_{k}' + for k in range(len(self.ESR_frequencies))] + elif name in ['combined_flips', 'combined_flip_probability', + 'filtered_combined_flips', + 'filtered_combined_flip_probability'] and \ + len(self.ESR_frequencies) > 1: + names += [f'{name}_{k}{k+1}' + for k in range(len(self.ESR_frequencies) - 1)] + elif name in ['filtered_flips', 'filtered_flip_probability'] and \ + len(self.ESR_frequencies) > 1: + for k in range(0, len(self.ESR_frequencies)): + if k > 0: + names.append(f'{name}_{k}_{k-1}{k}') + if k < len(self.ESR_frequencies) - 1: + names.append(f'{name}_{k}_{k}{k+1}') + return names + + @names.setter + def names(self, names): + self._names = names + + @property_ignore_setter + def shapes(self): + return tuple((self.samples,) if 'up_proportions' in name else () + for name in self.names) + + @property_ignore_setter + def units(self): + return ('', ) * len(self.names) + + @property + def ESR_frequencies(self): + """ESR frequencies to measure. + + For each ESR frequency, ``NMRParameter.ESR['shots_per_read']`` reads + are performed. + """ + 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, Iterable): + 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 SyntaxError(f'Subpulse type not allowed: {subpulse}') + ESR_frequencies.append(ESR_subfrequencies) + else: + raise SyntaxError(f'pulse type not allowed: {pulse}') + return ESR_frequencies + + @ESR_frequencies.setter + def ESR_frequencies(self, ESR_frequencies: List): + assert len(ESR_frequencies) == len(self.ESR['ESR_pulses']), \ + 'Different number of frequencies to ESR pulses.' + + updated_ESR_pulses = [] + for ESR_subpulses, ESR_subfrequencies in zip(self.ESR['ESR_pulses'], ESR_frequencies): + if isinstance(ESR_subpulses, str): + ESR_subpulses = copy(self.ESR[ESR_subpulses]) + elif isinstance(ESR_subpulses, Iterable): + ESR_subpulses = [ + copy(self.ESR[p]) if isinstance(p, str) else p + for p in ESR_subpulses] + + # Either both the subpulses and subfrequencies must be iterable, or neither are (XNOR) + assert \ + ( + isinstance(ESR_subpulses, Iterable) and + isinstance(ESR_subfrequencies, Iterable) + ) or ( + not (isinstance(ESR_subpulses, Iterable) or isinstance( + ESR_subfrequencies, Iterable)) + ), \ + 'Data structures for frequencies and pulses do not have the same shape.' + + if not isinstance(ESR_subpulses, Iterable): + ESR_subpulses = [ESR_subpulses] + if not isinstance(ESR_subfrequencies, Iterable): + ESR_subfrequencies = [ESR_subfrequencies] + + for pulse, frequency in zip(ESR_subpulses, + ESR_subfrequencies): + pulse.frequency = frequency + + updated_ESR_pulses.append(ESR_subpulses) + self.ESR['ESR_pulses'] = updated_ESR_pulses + + def analyse(self, traces: Dict[str, Dict[str, np.ndarray]] = None): + """Analyse flipping events between nuclear states and determine nuclear state + + Returns: + (Dict[str, Any]): Dict containing: + + * **results_read** (dict): `analyse_traces` results for each read + trace + * **up_proportions_{idx}** (np.ndarray): Up proportions, the + dimensionality being equal to ``NMRParameter.samples``. + ``{idx}`` is replaced with the zero-based ESR frequency index. + * **state_probability_{idx}** (np.ndarray): probability of measuring electron spin-up proportion + above the threshold_up_proportion when reading out the nucleus state + * Results from `analyse_flips`. These are: + + - flips_{idx}, + - flip_probability_{idx} + - combined_flips_{idx1}{idx2} + - combined_flip_probability_{idx1}{idx2} + + Additionally, each of the above results will have another result + with the same name, but prepended with ``filtered_``, and appended + with ``_{idx1}{idx2}`` if not already present. Here, all the + values are filtered out where the corresponding pair of + up_proportion samples do not have exactly one high and one low for + each sample. The values that do not satisfy the filter are set to + ``np.nan``. + + * **filtered_scans_{idx1}{idx2}**: + """ + if traces is None: + traces = self.traces + + results = {'results_read': []} + + if hasattr(self, 'threshold_voltage'): + threshold_voltage = getattr(self, 'threshold_voltage') + else: + # Calculate threshold voltages from combined read traces + high_low = analysis.find_high_low( + np.ravel([trace[self.channel_label] for pulse_name, trace in traces.items() + if pulse_name.startswith('read_initialize')])) + threshold_voltage = high_low['threshold_voltage'] + results['threshold_voltage'] = threshold_voltage + + # Extract points per shot from a single read trace + single_read_traces_name = f"{self.ESR['read_pulse'].name}[0]" + single_read_traces = traces[single_read_traces_name][self.channel_label] + points_per_shot = single_read_traces.shape[1] + + self.read_traces = np.zeros((len(self.ESR_frequencies), self.samples, + self.ESR['shots_per_frequency'], + points_per_shot)) + up_proportions = np.zeros((len(self.ESR_frequencies), self.samples)) + state_probability = np.zeros(len(self.ESR_frequencies)) + threshold_up_proportion = np.zeros(len(self.ESR_frequencies)) + for f_idx, ESR_frequency in enumerate(self.ESR_frequencies): + for sample in range(self.samples): + # Create array containing all read traces + read_traces = np.zeros( + (self.ESR['shots_per_frequency'], points_per_shot)) + for shot_idx in range(self.ESR['shots_per_frequency']): + # Read traces of different frequencies are interleaved + traces_idx = f_idx + shot_idx * len(self.ESR_frequencies) + traces_name = f"{self.ESR['read_pulse'].name}[{traces_idx}]" + read_traces[shot_idx] = traces[traces_name][self.channel_label][sample] + self.read_traces[f_idx, sample] = read_traces + read_result = analysis.analyse_traces( + traces=read_traces, + sample_rate=self.sample_rate, + t_read=self.t_read, + t_skip=self.t_skip, + threshold_voltage=threshold_voltage) + up_proportions[f_idx, sample] = read_result['up_proportion'] + results['results_read'].append(read_result) + + if self.threshold_up_proportion is None: + threshold_up_proportion[f_idx] = analysis.determine_threshold_up_proportion_single_state( + up_proportions_arr=up_proportions[f_idx], + shots_per_frequency=self.ESR['shots_per_frequency']) + else: + threshold_up_proportion[f_idx] = self.threshold_up_proportion + + state_probability[f_idx] = np.mean(up_proportions[f_idx] >= threshold_up_proportion[f_idx]) + + if len(self.ESR_frequencies) > 1: + results[f'up_proportions_{f_idx}'] = up_proportions[f_idx] + results[f'state_probability_{f_idx}'] = state_probability[f_idx] + results[f'threshold_up_proportion_{f_idx}'] = threshold_up_proportion[f_idx] + else: + results['up_proportions'] = up_proportions[f_idx] + results['state_probability'] = state_probability[f_idx] + results['threshold_up_proportion'] = threshold_up_proportion[f_idx] + + # Add singleton dimension because analyse_flips_old handles 3D up_proportions + up_proportions = np.expand_dims(up_proportions, 1) + results_flips = analysis.analyse_flips_old( + up_proportions_arrs=up_proportions, + threshold_up_proportion=self.threshold_up_proportion, + shots_per_frequency=self.ESR['shots_per_frequency']) + # Add results, only choosing first element so its no longer an array + results.update({k: v[0] for k, v in results_flips.items()}) + return results diff --git a/silq/pulses/pulse_sequences.py b/silq/pulses/pulse_sequences.py index c9ae2f898..09832025a 100644 --- a/silq/pulses/pulse_sequences.py +++ b/silq/pulses/pulse_sequences.py @@ -1168,41 +1168,35 @@ def generate(self): class NMRCPMGPulseSequence(NMRPulseSequence): - """`PulseSequenceGenerator` for nuclear magnetic resonance (NMR). + """`PulseSequenceGenerator` for nuclear magnetic resonance CP sequences. - This pulse sequence can handle many of the basic pulse sequences involving - NMR. The pulse sequence is generated from its pulse settings attributes. + This pulse sequence handles pulse sequences that involve CP experiments, + where a train of refocusing pulses is applied after preparing the spin state along the + y axis. The main difference between this pulse sequence and a normal + NMRPulseSequence sequence is that this pulse sequence accounts for the difference in + interdelays between the first pi/2-pi pulses and the refocusing pulses. + + 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 any pre_pulses defined in ``NMRCPMGPulseSequence.pre_pulses``. + 2. Perform CPMG NMR sequence - 1. Perform stage pulse ``NMRPulseSequence.NMR['stage_pulse']``. + 1. Perform stage pulse ``NMRCPMGPulseSequence.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. + in ``NMRCPMGPulseSequence.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']``. + ``NMRCPMGPulseSequence.NMR['pre_delay']``, delays between the first NMR pulses (typically pi/2) is + ``NMRCPMGPulseSequence.NMR['inter_delay']``, and the delay + between refocusing pulses (pi pulses) is 2*NMRCPMGPulseSequence.NMR['inter_delay']. + The delay after the final + NMR pulse is ``NMRCPMGPulseSequence.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 + Refer to NMRPulse to learn about what this sequence does. Parameters: NMR (dict): Pulse settings for the NMR part of the pulse sequence. @@ -1217,6 +1211,10 @@ class NMRCPMGPulseSequence(NMRPulseSequence): 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']``. + + NOTE THAT THE FIRST PULSE IN THIS STRING NEEDS TO BE A PI/2 NMR PULSE,i.e + NMR['NMR_pulses'] = [['NMR_pi_half']+N*['NMR_pi']+['NMR_pi_half']] + * ``pre_delay`` (float): Delay after start of ``stage`` pulse, until first NMR pulse. * ``inter_delay`` (float): Delay between successive NMR pulses. @@ -1224,29 +1222,13 @@ class NMRCPMGPulseSequence(NMRPulseSequence): 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`. + Refer to NMRPulse to learn about the items it contains: 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: + contrast, and to measure dark counts and hence ESR contrast. + Refer to NMRPulse to learn about the items it contains: - * ``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. @@ -1263,6 +1245,8 @@ class NMRCPMGPulseSequence(NMRPulseSequence): Notes: For given pulse settings, `NMRPulseSequence.generate` will recreate the pulse sequence from settings. + + """ def __init__(self, pulses=[], **kwargs): super().__init__(pulses=pulses, **kwargs)