Time & frequency scans¶
Scans over a range of time values or frequency values are common. To simplify the process of writing these
types of scans, the scan framework provides three base classes from which a scan may inherit. These
base classes are TimeScan
,
FreqScan
, and
TimeFreqScan
. Additionally, any of these scans
can also use auto-tracking or set self._x_offset
to offset the scan points. See Relative scan ranges and auto tracking.
Time scans¶
Time scans create a configurable GUI argument for entering a range of times to scan over. To create a time
scan, simply inherit from the TimeScan
class and call
scan_arguments()
in build()
.
from scan_framework.scans import *
class MyTimeScan(Scan1D, TimeScan, EnvExperiment):
def build(self):
...
# scan_arguments() creates an additional GUI argument for entering a time range
self.scan_arguments(
# the GUI argument can be fully configured via the 'times' argument
times={
'start':0,
'stop':100*us,
'npoints':50,
'unit': 'us',
'scale': us,
'global_step': 1*us,
'ndecimals': 0
}
)
When creating a time scan it is not necessary to define the get_scan_points()
callback method; the framework will automatically scan over the GUI argument named ‘times’.
The scan model that is registered with a time scan can also inherit from the
TimeModel
class which automatically
sets the x_units
and
x_label
attributes of the scan model.
Frequency scans¶
Frequency scans create a configurable GUI argument for entering a range of frequencies to scan over. To create a
frequency scan, simply inherit from the FreqScan
class and call
scan_arguments()
in build()
.
from scan_framework.scans import *
from scan_framework.models import *
class MyFreqScan(Scan1D, FreqScan, EnvExperiment):
def build(self):
super().build()
# scan_arguments() creates an additional GUI argument for entering a frequency range
# the range of frequencies is set to the attribute named 'frequencies' (i.e. self.frequencies)
self.scan_arguments(
# the GUI argument can be fully configured via the 'frequencies' argument
frequencies={
'start': -0.1 * MHz,
'stop': 0.1 * MHz,
'npoints': 50,
'unit': 'MHz',
'scale': MHz,
'global_step': 0.1*MHz,
'ndecimals': 1
}
)
def prepare(self):
# -- like all scans, frequency scans can also use auto-tracking to center a relative scan
# range about a fixed frequency
# Create a default fitted frequency for the first run when no fits have been performed yet
self.set_dataset('example.defaults.frequency', 1*MHz, broadcast=True)
model = ScanModel(self,
namespace='example',
main_fit='frequency',
# tell framework to use default value above when no fit exists
default_fallback=True
)
self.register_model(model, auto_track='fit', measurement=True)
When creating a frequency scan it is not necessary to define the get_scan_points()
callback method; the framework will automatically scan over the GUI frequencies argument.
The scan model that is registered with a frequency scan can also inherit from the
FreqModel
class which automatically
sets the x_units
and
x_label
attributes of the scan model.
Time/frequency scans¶
Time/frequency scans are provided for scans that need to scan over either a range of frequencies or
a range of times. This is useful for scans of atomic transitions which need to find both the transition
frequency and the appropriate pi time for the transition. Creating a TimeFreqScan
significantly simplifies these types of scans. Inheriting from
TimeFreqScan
Creates two GUI arguments for entering either a range of frequencies or a range of times.
Creates a GUI argument for specifying if the scan should scan over the range of frequencies or times.
Centers the frequency range about the last fitted frequency when auto-tracking is used.
Determines the scan points automatically (
get_scan_points()
does not need to be implemented).Uses the last fitted pi time for frequency scans when using auto-tracking.
Uses the last fitted frequency for time scans when using auto-tracking.
Provides a GUI argument to enter the pulse time for frequency scans when auto-tracking is not being used.
Provides a GUI argument to enter the frequency for time scans when auto-tracking is not being used.
Passes both the frequency and time as arguments to the
measure()
method.
To create a Time/frequency scan, simply inherit from the
TimeFreqScan
class and call
scan_arguments()
in build()
. If you are also
using auto-tracking, register a single auto-tracking scan model and use the
type
attribute in the scan model to dynamically
determine the fit function, main fit, etc based on the type (frequency or time) of scan being performed.
For a full example of a TimeFreqScan
class that uses
auto-tracking, see the example below.
Note
The scan model that is registered for a time/frequency scan can also inherit from the
TimeFreqModel
class which automatically
sets the x_units
and
x_label
attributes of the scan model.
Note
Scan models can also be registered with the bind
argument set to True in time/frequency scans.
i.e. self.register_model(my_model_instance, bind=True)
. This will cause the model to be
re-bound after its type
attribute is set to the current scan type (time or frequency). This
is useful if you need to create a dynamic namespace that includes a token for the type of scan.
e.g. namespace = 'microwaves.%type'
. %type
will be replaced by either ‘frequency’ or ‘time’ when
the model is registered with bind=True
.
from scan_framework.scans import *
from scan_framework.models import *
from scan_framework.analysis.curvefits import AtomLine, Sine
import random
class MicrowaveScan(Scan1D, TimeFreqScan, EnvExperiment):
"""Microwave scan
Scans frequencies and pulse times of microwave transitions
"""
def build(self, **kwargs):
super().build(**kwargs)
# The atomic transition, identified by an integer to simply logic in
# the "measure()" method
self.setattr_argument('transition', EnumerationValue(
['0', '1', '2', '3', '4', '5', '6', '7'],
default='1'))
# scan settings, scan ranges, etc.
self.scan_arguments(
# frequency range can be customized
frequencies={
'start': -0.3*MHz,
'stop': 0.3*MHz
},
# time range can also be customized
times={
'start': 0*us,
'stop': 20*us
}
)
# create devices, instantiate libs, etc.
...
def prepare(self):
# convert string transition to integer for the "measure()" method
self.transition = int(self.transition)
# create and register the scan model
self.model = MicrowavesScanModel(self,
# set the model's transition attribute to the selected transition in the GUI.
# this allows the %transition token in the model namespace to be replaced
# by the current transition.
transition=self.transition
)
self.register_model(self.model,
# calculate statistics and store all data to the datasets
measurement=True,
# perform a final fit to the data
fit=True,
# points will be offset by this model's last fitted frequency value
# (a.k.a. it's main fit)
auto_track='fit')
@kernel
def initialize_devices(self):
self.core.reset()
@kernel
def measure(self, time, frequency):
self.cooling.doppler()
if self.transition >= 2:
self.microwaves.transition_1()
if self.transition >= 3:
self.microwaves.transition_2()
if self.transition >= 4:
self.microwaves.transition_3()
if self.transition >= 5:
self.microwaves.transition_4()
# pulse dds
self.microwaves.set_frequency(frequency)
self.microwaves.pulse(time)
# detect
counts = self.detection.detect()
return counts
class MicrowavesScanModel(TimeFreqModel):
"""Microwave scan model
Processes data from microwave scans
"""
# %transition will be replaced by the transition selected in the GUI
namespace = 'microwaves.%transition'
y_label = 'Counts'
# scales for formatting fit params printed to the log window
scales = {
'f': {
'scale': MHz,
'unit': 'MHz'
},
'phi': {
'scale': 3.14159,
'unit': 'pi'
},
'f0': {
'scale': MHz,
'unit': 'MHz'
},
'Omega0': {
'scale': MHz,
'unit': 'MHz'
},
'T': {
'scale': us,
'unit': 'us'
}
}
@property
def main_fit(self):
if self.type == 'frequency':
# save fit param 'f0' to dataset named 'frequency'
return ['f0', 'frequency']
if self.type == 'time':
# save calculated fit param 'pi_time'
return 'pi_time'
def before_validate(self, fit):
# calculate the fit param 'pi_time' from the fit param 'f'
if self.type == 'time':
fit.fitresults['pi_time'] = 1/(2*fit.fitresults['f'])
@property
def fit_function(self):
if self.type == 'frequency':
# frequency scans use the AtomLine fit function
return AtomLine
elif self.type == 'time':
# times scans use the Sine fit function
return Sine
else:
raise Exception('Unknown scan type {}'.format(self.type))
@property
def man_scale(self):
# fit parameter scales, used by analysis.curvefits while fitting
if self.type == 'frequency':
return {
'A': 1,
'Omega0': 1 / (10 * us),
'T': 1 * us,
'f0': 1 * GHz,
'y0': 1
}
else:
return {
'A': 10,
'f': 1 / (10 * us),
'phi': 1,
'y0': 1
}
@property
def guess(self):
# fit parameter guesses, used by analysis.curvefits while fitting
if self.type == 'time':
if self.transition in [1, 3, 5, 6, 7]:
return {
'phi': 0.5*3.14159,
'y0': 5,
'A': 5,
}
else:
return {
'phi': 1.5*3.14159,
'y0': 5,
'A': 5,
}
else:
return {
'T': self.get('pi_time', archive=False)
}