Relative scan ranges and auto tracking

Relative scan ranges

Often it is desirable to specify a range of scan points that is relative to some fixed offset. For example, scans of atomic resonances vs probe frequencies are often most useful when the probe frequency range is specified as a range of frequencies relative to some fixed atomic frequency. A scan range that is much smaller than the fixed frequency can then easily be entered in the dashboard without having to keep track of which digit in a large number corresponds to a certain scale (e.g. MHz or KHz portions of a GHz value).

This is easily accomplished by setting self._x_offset in the scan class.

def build(self):
    ...

    # range of frequencies relative to 1.8121*GHz
    self.setattr_argument('frequencies', Scannable(
        default=RangeScan(
            start=-0.1*MHz,
            stop=0.1*MHz,
            npoints=50
        ), unit='MHz', scale=MHz))

def prepare(self):
    # offset all scan points by this value
    self._x_offset = 1.8121*GHz

def get_scan_points(self):
    return self.frequencies

In the above example, a narrow range of 200 kHz is being scanned about a center frequency of 1.8121 GHz. This narrow range is displayed in the GUI for editing and each realtive scan point value will have 1.81121 GHz added to it automatically by the framework before the scan is executed.

Auto-tracking

When working with models, _x_offset can be determined automatically by the framework from the last fitted value for a scan. This is useful because the center value of an absolute scan range may change over time, such as in the case of atomic transition frequencies. Using auto-tracking allows the scan to naturally follow any drifts in the value being fit as it is periodically run. To use auto-tracking in a scan, first create a scan model that has the main_fit attribute defined. Then register the scan model with the auto_track attribute set.

def prepare(self):
    # Fetch the current dataset given by my_model.main_fit
    # and offset every scan point by this value.
    my_model = MyScanModel(self)
    self.register_model(my_model, auto_track='fit')

When auto_track='fit' or auto_track'fitresults' are set, the framework automatically offsets the scan points by the main fit of the scan. If auto_track='fit' is set, the most recently fitted value of main_fit is fetched from the datasets (i.e. from a previous run of the scan) and used to offset the scan points, while setting auto_track='fitresults' causes the fitted value that was just found by the scan to be used (which has not yet been saved to the datasets).

Note

Auto tracking can be disabled entirely in either a FreqScan or a TimeFreqScan by setting self.enable_auto_tracking = False in the scan.

Setting auto_track='fitresults' is useful in cases where a separate sub-scan is run in the measure() method of a top-level scan and the value returned by the measure() method is a parameter that is fit by the sub-scan.

As an example, and a more advanced usage of the scan framework:

from scan_framework.scans import *
import experiments.scans.tickle_scan as scan
from lib.models.rf_resonator_model import *
from scan_framework.models import *


class RFResonatorScan(Scan1D, EnvExperiment):
    """RF Resonator Scan

    Scans over RF synth frequencies to find the resonant frequency of the resonator
    between the RF synthesizer output and the ion trap RF electrodes.

    A separate tickle scan is performed at each scan point (RF synth frequency) to find the

    """
    # top-level scan is run on the host (sub-scan is run on the core device)
    run_on_core = False

    def build(self):
        super().build()

        # RF synthesizer device
        self.rf_synth = self.get_device('rf_synth')

        # tickle scan
        self.tickle_scan = scan.TickleScan(self,
           # don't save any fitted values since this is just a sub-scan
           fit_options='Fit',
           # auto-center each sub-scan about the fitted value from the previous scan point
           auto_track=True,
           # don't display fitted values in the log window of the dashboard
           enable_reporting=False)

        self.scan_arguments()

        # range of absolute RF synthesizer frequencies
        self.setattr_argument('rf_frequencies', Scannable(
            default=RangeScan(
                start=63.72 * MHz,
                stop=63.74 * MHz,
                npoints=10
            ),
            unit='MHz',
            scale=1 * MHz,
            ndecimals=4
        ), group='Scan Range')

        # range of relative tickle frequencies
        self.setattr_argument('frequencies', Scannable(
            default=RangeScan(
                start=-0.1 * MHz,
                stop=0.1 * MHz,
                npoints=30
            ),
            unit='MHz',
            scale=1 * MHz,
            ndecimals=4
        ), group='Scan Range')

        # used internally for auto-tracking
        self.tracking_seeded = False

    def prepare(self):
        # set the relative scan points of the sub-scan
        self.tickle_scan.frequencies = self.frequencies
        self.tickle_scan.prepare()

        # register the top-level scan model
        self.model = RfResonatorScanModel(self)
        self.register_model(self.model, measurement=True, fit=True)

    # top-level scan points (RF synth frequencies)
    def get_scan_points(self):
        return self.rf_trap_frequencies

    def set_scan_point(self, i_point, point):
        # set the RF synth frequency
        self.core.break_realtime()
        self.rf_synth.set(point)

    def measure(self, rf_trap_frequency) -> TInt64:
        # find the secular frequency that results for the current RF frequency driving the resonator
        self.tickle_scan.run()

        # fit is not available in datasets since tickle fit's aren't being saved
        if self.tickle_scan.model.fit_valid:
            # start auto-tracking the last fitted tickle freq once we have a good fit
            self.tracking_seeded = True
            return self.tickle_scan.model.fit.frequency
        else:
            return 0

    def after_scan_point(self, i_point, point):
        if self.tracking_seeded:
            # center the next sub-scan about the fitted tickle freq for the
            # current scan point
            self.tickle_scan.auto_track = 'fitresults'

Each time the sub-scan is run within the top-level scan in the example above, its scan range will be automatically centered around the value of the fitted tickle frequency from the previous scan point. This allows the sub-scan range to stay appropriately centered about the fitted value at each scan point, which changes with each scan point. This method avoids having to specify a very large scan range that spans all the relevant frequencies of the sub-scan.