2D Scans

2D scans can be created by inheriting from the Scan2D class. 2D scans are similar to 1D scans with the following changes:

  1. Two scan models are registered

  2. Two sets of scan points are defined and returned by get_scan_points() (one for each dimension).

  3. The calculate_dim0(self, dim1_model) callback must be implemented in the scan.

  4. The point and i_point parameters passed to callback methods will be lists with two entries containing the point and point index of each dimension.

To create a 2D scan, inherit from the Scan2D class, register two models, and return a list from get_scan_points() with two entries containing the scan points for both dimensions. The dimension argument passed to register_model() specifies the dimension of the model. Dimension 1 is the sub-scan, and dimension 0 is the top level scan.

The calculate_dim0 Callback

The calculate_dim0(self, dim1_model) callback returns a fitted parameter or a calculated value along with the error in that value after a sub-scan completes. After a sub-scan completes and a fit has been performed on that sub-scan , calculate_dim0() will be called and passed the dimension 1 model. The dimension 1 model can then be used in calculate_dim0() to fetch the fitted parameters or other data needed to calculate the value and error to return. The value returned will be plotted as the y-value in the dimension 0 plot. The error returned will weight that value when the final fit is performed along dimension 0 using the y-values and errors returned by calculate_dim0(). The corresponding x-values used by the dimension 0 fit are the scan points of the top level (dimension 0) scan.

Example 2D Scan

An example of a 2D scan that scans over the RF trap frequency and performs a sub-scan over tickle frequencies might look like:

from scan_framework.scans import *
from scan_framework.models import *
from lib.cooling import *
from lib.detection import *

class RfResonatorScan(Scan2D, EnvExperiment):

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

        # devices
        self.setattr_device('ttl_tickle')
        self.setattr_device('dds_tickle')

        # libs
        self.cooling = Cooling(self)
        self.detection = Detection(self)

        # arguments
        self.setattr_argument('rf_frequencies', Scannable(
            default=RangeScan(
                start=64.44 * MHz,
                stop=64.48 * MHz,
                npoints=20
            ),
            unit='MHz',
            scale=1 * MHz,
            ndecimals=4
        ), group='Scan Range')
        self.setattr_argument('tickle_frequencies', Scannable(
            default=RangeScan(
                start=4.6 * MHz,
                stop=4.8 * MHz,
                npoints=50
            ),
            unit='MHz',
            scale=1 * MHz,
            ndecimals=4
        ), group='Scan Range')

        # scan arguments
        self.scan_arguments()

    def prepare(self):
        # Dimension 0 model (top level, RF trap frequencies)
        self.rf_model = ScanModel(self,
                                  namespace="rf_resonator",
                                  fit_function = fitting.Lor,
                                  main_fit = 'x0')
        self.register_model(self.rf_model,
                            dimension=0,
                            # Peform a final fit on the fitted parameters from each sub-scan
                            fit=True,
                            set=True)

        # Dimension 1 model (sub-scan level, tickle frequencies)
        self.tickle_model = ScanModel(self,
                                      fit_function = fit_functions.SincInv
                                      main_fit = 'frequency')
        self.register_model(self.tickle_model,
                            dimension=1,
                            # Data is collected by the dimension 1 model only
                            measurement=True,
                            # Do save the fitted param values to the current_scan namespace
                            set=True,
                            # Don't save the fitted frequency as the current tickle freq
                            save=False)

    def get_scan_points(self):
        # assign the trap frequencies and tickle frequencies as the scan points
        return [
            self.rf_frequencies,  # dimension 0 (trap freqs)
            self.tickle_frequencies,  # dimension 1 (tickle freqs)
        ]

    @kernel
    def set_scan_point(self, i_point, point):
        trap_freq = point[0]
        tickle_freq = point[1]

        # set the trap frequency at the start of the tickle sub-scan
        self.core.break_realtime()
        if i_point[1] == 0:
            self.dds_rf.set(trap_freq)
            delay(3*us)

        # set the tickle frequency
        self.dds_tickle.set(tickle_freq)
        delay(3*us)

    @kernel
    def measure(self, point):
        # cool
        self.cooling.doppler()

        # pulse the tickle TTL
        self.ttl_tickle.pulse(100*us)

        # detect
        counts = self.detection.detect()
        return counts

    def calculate_dim0(self, dim1_model):
        # plot this dimension 1 fitted value in the dimension 0 plot
        param = dim1_model.fit.params.frequency
        # weight final fit by error in this dimension 1 fit param
        error = dim1_model.fit.errs.frequency_err
        return param, error