Skip to content

Commit

Permalink
Creation of the CPMG Parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
Ifefu committed Jul 1, 2021
1 parent d6f00e2 commit 5b90330
Show file tree
Hide file tree
Showing 3 changed files with 344 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ experiments_folder.txt
_modules
*/make.bat
Qcodes

venv/
318 changes: 315 additions & 3 deletions silq/parameters/acquisition_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
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, \
property_ignore_setter

__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__)
Expand Down Expand Up @@ -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
Expand All @@ -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],
Expand Down Expand Up @@ -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
Loading

0 comments on commit 5b90330

Please sign in to comment.