Single FID Example

This example shows an overview of the basic functionality of the Blackchirp python module. It uses data acquired using the UC Davis Ka band (26.5 - 40 GHz) CP-FTMW spectrometer.

To get started, first ensure that the module has been installed with pip install blackchirp. It is recommended to import the main blackchirp classes with from blackchirp import *. The cell below is configured to import from the development version of the module if this example notebook is run from the blackchirp source directory.

[1]:
from blackchirp.src.blackchirp import * # replace with from blackchirp import *
from matplotlib import pyplot as plt

Loading and Inspecting an Experiment

In this example, we will use a CP-FTMW spectrum of methyl tert-butyl ether, which is assumed to be in the directory example-data/mtbe. This directory contains the contents of the Blackchirp data, which was copied from the original data storage location.

[2]:
ll example-data/mtbe/
total 40
-rw-r--r-- 1 kncrabtree 1110 Jun  4 16:42 auxdata.csv
-rw-r--r-- 1 kncrabtree  663 Jun  4 16:42 chirps.csv
-rw-r--r-- 1 kncrabtree  127 Jun  4 16:42 clocks.csv
drwxr-xr-x 2 kncrabtree 4096 Jun  4 16:42 fid/
-rw-r--r-- 1 kncrabtree  118 Jun  4 16:42 hardware.csv
-rw-r--r-- 1 kncrabtree 7285 Jun  4 16:42 header.csv
-rw-r--r-- 1 kncrabtree  183 Jun  4 16:42 log.csv
-rw-r--r-- 1 kncrabtree   27 Jun  4 16:42 objectives.csv
-rw-r--r-- 1 kncrabtree  118 Jun  4 16:42 version.csv

The BCExperiment class is used to read in these files and construct python objects for them. Since all Blackchirp data is written in CSV format, the files are read in using pandas.read_csv and are available as pandas DataFrame objects with the same name as the corresponding CSV file. To load an experiment, pass the appropriate path to BCExperiment. This path should be the folder which contains the version.csv file for the desired experiment. Once loaded, the contents of the csv files may be inspected easily. Here we show the header.

[3]:
exp = BCExperiment('./example-data/mtbe/')

exp.header
[3]:
ObjKey ArrayKey ArrayIndex ValueKey Value Units
0 ChirpConfig <NA> ChirpInterval 20 μs
1 ChirpConfig <NA> PostGate -0.17 μs
2 ChirpConfig <NA> PostProtection 0.15 μs
3 ChirpConfig <NA> PreGate 0.5 μs
4 ChirpConfig <NA> PreProtection 0.1 μs
... ... ... ... ... ... ...
176 RfConfig <NA> CommonUpDownLO false
177 RfConfig <NA> DownconversionSideband LowerSideband
178 RfConfig <NA> ShotsPerClockConfig 0
179 RfConfig <NA> TargetSweeps 1
180 RfConfig <NA> UpconversionSideband LowerSideband

181 rows × 6 columns

Just like in the header.csv file itself, each entry is associated with an ObjKey and a ValueKey, and in some cases also an ArrayKey and ArrayIndex. The combination of these 4 values can be used to select any particular row, as shown in more detail below. Each entry has a Value and Units associated with it.

For such a large DataFrame, Jupyter notebooks typically compress the output. To get a brief overview of what information is available, use the BCExperiment.header_unique_keys() function, which returns a set containing the unique keys in the table.

[4]:
exp.header_unique_keys?
Signature: exp.header_unique_keys() -> 'set[str]'
Docstring:
Fetch all unique ObjKeys in experiment header

Returns:
    List of unique header keys
File:      ~/github/blackchirp/src/python/blackchirp/src/blackchirp/blackchirpexperiment.py
Type:      method
[5]:
exp.header_unique_keys()
[5]:
{'ChirpConfig',
 'Experiment',
 'FtmwConfig',
 'FtmwDigitizer.0',
 'PulseGenerator.0',
 'PulseGenerator.1',
 'RfConfig'}

To view only data associated with one of these keys, use the BCExperiment.header_rows() function:

[6]:
exp.header_rows?
Signature:
exp.header_rows(
    objKey: 'str' = None,
    valKey: 'str' = None,
    arrKey: 'str' = None,
) -> 'pd.DataFrame'
Docstring:
Fetch rows from the header file matching conditions

Filters rows in the header according to ObjKey, ValueKey, and ArrayKey.
Any combination of these (or none) may be specified to filter.

Args:
    objKey: Object key in header
    valKey: Value key in header
    arrKey: Array key in header

Returns:
    DataFrame with matching wors
File:      ~/github/blackchirp/src/python/blackchirp/src/blackchirp/blackchirpexperiment.py
Type:      method

For instance, to obtain only the settings related to the Pulse Generator:

[7]:
exp.header_rows('PulseGenerator.0')
[7]:
ObjKey ArrayKey ArrayIndex ValueKey Value Units
48 PulseGenerator.0 <NA> PulseGenEnabled true
49 PulseGenerator.0 <NA> PulseGenMode Triggered_Rising
50 PulseGenerator.0 <NA> RepRate 5 Hz
51 PulseGenerator.0 Channel 0 ActiveLevel ActiveHigh
52 PulseGenerator.0 Channel 0 Delay 0 μs
... ... ... ... ... ... ...
126 PulseGenerator.0 Channel 7 Mode Normal
127 PulseGenerator.0 Channel 7 Name Ch8
128 PulseGenerator.0 Channel 7 Role None
129 PulseGenerator.0 Channel 7 SyncChannel 0
130 PulseGenerator.0 Channel 7 Width 1 μs

83 rows × 6 columns

To see only the pulse widths:

[8]:
exp.header_rows('PulseGenerator.0','Width')
[8]:
ObjKey ArrayKey ArrayIndex ValueKey Value Units
60 PulseGenerator.0 Channel 0 Width 650 μs
70 PulseGenerator.0 Channel 1 Width 150 μs
80 PulseGenerator.0 Channel 2 Width 20 μs
90 PulseGenerator.0 Channel 3 Width 2.019 μs
100 PulseGenerator.0 Channel 4 Width 1 μs
110 PulseGenerator.0 Channel 5 Width 1 μs
120 PulseGenerator.0 Channel 6 Width 1 μs
130 PulseGenerator.0 Channel 7 Width 1 μs

And finally, the BCExperiment.header_value() function retrieves one particular value from the table. A corresponding BCExperiment.header_unit() function can be used to retrieve the unit, if desired. Note that the value is returned as a string, so it may need to be explicitly cast to an int or float if the value will be used in a calculation.

[9]:
exp.header_value?
Signature:
exp.header_value(
    objKey: 'str',
    valKey: 'str',
    idx: 'int' = 0,
    arrKey: 'str' = None,
) -> 'str'
Docstring:
Fetch one value from header

The objKey and valKey (and arrKey, if specified) are used to filter the header.
Then the idx value is used to determine which row to return. If a match is not
found, an empty string is returned

Args:
    objKey: Object key in header
    valKey: Value key in header
    idx: Row number to return (optional)
    arrKey: Array key in header (optional)

Returns:
    Matching value or empty string
File:      ~/github/blackchirp/src/python/blackchirp/src/blackchirp/blackchirpexperiment.py
Type:      method
[10]:
exp.header_value('PulseGenerator.0','Width',3),exp.header_unit('PulseGenerator.0','Width',3)
[10]:
('2.019', 'μs')

Other csv files are similarly accessible.

[11]:
exp.chirps
[11]:
Chirp Segment StartMHz EndMHz DurationUs Alpha Empty
0 0 0 4895 1520 2 -1687.5 False
1 1 0 4895 1520 2 -1687.5 False
2 2 0 4895 1520 2 -1687.5 False
3 3 0 4895 1520 2 -1687.5 False
4 4 0 4895 1520 2 -1687.5 False
5 5 0 4895 1520 2 -1687.5 False
6 6 0 4895 1520 2 -1687.5 False
7 7 0 4895 1520 2 -1687.5 False
8 8 0 4895 1520 2 -1687.5 False
9 9 0 4895 1520 2 -1687.5 False
10 10 0 4895 1520 2 -1687.5 False
11 11 0 4895 1520 2 -1687.5 False
12 12 0 4895 1520 2 -1687.5 False
13 13 0 4895 1520 2 -1687.5 False
14 14 0 4895 1520 2 -1687.5 False
15 15 0 4895 1520 2 -1687.5 False
16 16 0 4895 1520 2 -1687.5 False
17 17 0 4895 1520 2 -1687.5 False
18 18 0 4895 1520 2 -1687.5 False
19 19 0 4895 1520 2 -1687.5 False

FID data and Fourier Transforms

The CP-FTMW data for an experiment are stored in a BCFTMW object which is accessible as BCExperiment.ftmw. This object can be used to view data about the available FIDs and load them from disk. For a single FID acquisition like this, the FID is loaded with BCFTMW.get_fid(), which returns a BCFid object. The BCFID.ft() method computes the Fourier transform of that FID. As a quick example:

[12]:
x,y = exp.ftmw.get_fid().ft()

fig,ax = plt.subplots(figsize=(10,3))
ax.plot(x,y)
ax.set_xlabel('Frequency (MHz)')
ax.set_xlim(26500,40000)
[12]:
(26500.0, 40000.0)
../../_images/python_notebooks_single-fid_21_1.png

Taking a step back, the FID itself can be stored and visualized. The FID itself has BCFid.y() and BCFid.xy() methods which return arrays containing the FID data in units of V, and in the latter case, also the time array in units of s. For this FID, the chirp takes place from roughly 0.75 - 3.75 μs.

[13]:
fid = exp.ftmw.get_fid()

fidx, fidy = fid.xy()
fig,ax = plt.subplots(figsize=(10,3))
ax.plot(fidx*1e6,fidy,label=f'{int(fid.shots)} shots')
ax.set_xlabel('Time (μs)')
ax.set_ylabel('FID (V)')
ax.legend(frameon=False)
[13]:
<matplotlib.legend.Legend at 0x7f556485d1c0>
../../_images/python_notebooks_single-fid_23_1.png

At this point it is important to note that fidy is a 2D numpy array. The second axis corresponds to the frame number. In this acquisition, there is only 1 frame, but for an acquisition configured with multiple records this number may be larger. A single frame can then be selected by slicing (e.g., fidy[:,3]).

[14]:
fidy.shape
[14]:
(750000, 1)

The contents of the fidparams.csv file are available as an attribute of the BCFTMW object.

[15]:
exp.ftmw.fidparams
[15]:
spacing probefreq vmult shots sideband size
index
0 2.000000e-11 40960 0.000002 707340 LowerSideband 750000
1 2.000000e-11 40960 0.000002 89820 LowerSideband 750000
2 2.000000e-11 40960 0.000002 179840 LowerSideband 750000
3 2.000000e-11 40960 0.000002 269840 LowerSideband 750000
4 2.000000e-11 40960 0.000002 359860 LowerSideband 750000
5 2.000000e-11 40960 0.000002 449860 LowerSideband 750000
6 2.000000e-11 40960 0.000002 539860 LowerSideband 750000
7 2.000000e-11 40960 0.000002 629860 LowerSideband 750000

This experiment contains 2 FIDs: FID 0 is the final set of data and FID 1 was a backup that was taken 15 minutes into the acquisition. The BCFTMW.get_fid() function takes an optional argument that specifies the FID number to retrieve. We can therefore access the backup version with get_fid(1). The code below loads the backup and plots its FT together with the total, showing the decrease in noise level with increasing shots.

[16]:
x2,y2 = exp.ftmw.get_fid(1).ft()
fig,ax = plt.subplots(figsize=(10,3))
ax.plot(x2,y2,label='FID 1')
ax.plot(x,y,label='FID 0')
ax.set_xlabel('Frequency (MHz)')
ax.set_xlim(36000,36010)
ax.set_ylim(0,0.15)
ax.legend()
[16]:
<matplotlib.legend.Legend at 0x7f5562d4b580>
../../_images/python_notebooks_single-fid_29_1.png

The BCFid.ft() function also allows for customizing the FID processing options like those in the Blackchirp program itself. The default values are read in from processing.csv, which is converted into a python dictionary named proc which is an attribute of BCFTMW. One notable difference in behavior: in the python module, points within “AutoscaleMHz” of the probe frequency are set to 0 to accommodate automatic autoscaling of the plot, while in Blackchirp itself those points are still shown but are not included when computing the vertical range of the FT.

[17]:
exp.ftmw.proc
[17]:
{'AutoscaleIgnoreMHz': '250',
 'FidEndUs': '15',
 'FidExpfUs': '0',
 'FidRemoveDC': 'false',
 'FidStartUs': '3.35',
 'FidWindowFunction': 'None',
 'FidZeroPadFactor': '0',
 'FtUnits': '6'}

The BCFid.ft() function allows for any of these settings to be overridden.

[18]:
fid.ft?
Signature:
fid.ft(
    *,
    start_us: 'float' = None,
    end_us: 'float' = None,
    winf: 'str' = None,
    zpf: 'int' = None,
    rdc: 'bool' = None,
    expf_us: 'float' = None,
    autoscale_MHz: 'float' = None,
    units_power: 'int' = None,
    frame: 'int' = None,
) -> 'tuple[np.ndarray, np.ndarray]'
Docstring:
Compute the Fourier transform of the FID

By default, this computes the FT for each frame in the FID using the settings
stored in the proc dictionary. This behavior can be overridden by specifying
any combination of the keyword arguments.

Args:
    start_us: Starting time, in μs. Points at earlier times are set to 0.
    end_us: Ending time, in μs. Points at later times are set to 0.
    winf: Window function applied to points between start and end.
        This is passed directly to `scipy.signal.get_window <https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.get_window.html>`_.
    zpf: Zero-padding factor (positive integer). If nonzero, the FID is
        padded with zeroes until its length reaches the next power of 2,
        Then, its length is further extended by 2\*\*zpf.
    rdc: If true, the average of the FID is subtracted before the FT is
        computed.
    expf_us: Time constant for an exponential decay filter, in μs.
    autoscale_MHz: Range of FT points to set to 0, relative to the Downconversion
        LO frequency. Useful for suppressing noise near DC.
    units_power: FT is scaled by 10\*\*units_power. For μV units, set
        units_power=6.
    frame: Apply FT to only the specified frame.

Returns:
    Frequency array (MHz), Intensity array

Raises:
    ValueError: If supplied arguments are invalid

Examples:
    Assuming a BCFid object named ``fid``::

        #default FT calculation
        x,y = fid.ft()

        #override some processing settings
        x,y = fid.ft(start_us=3.0,rdc=False,units_power=3)

        #compute ft for only frame 3 (assuming the number of frames is >=4)
        x,y = fid.ft(frame=3)

        #average all frames, then apply a custom window function
        fid.average_frames()
        p = 1.5
        sigma = len(fid)//5
        x,y = fid.ft(winf=('general_gaussian',p,sigma))
File:      ~/github/blackchirp/src/python/blackchirp/src/blackchirp/bcfid.py
Type:      method

For example, to apply an exponential filter and compare with the original data:

[19]:
expf = 5.0
x3,y3 = exp.ftmw.get_fid().ft(expf_us=expf)

fig,axes = plt.subplots(2,1,figsize=(10,6))
for ax in axes:
    ax.plot(x,y,label='No filter')
    ax.plot(x3,y3,label=f'expf = {expf:.2f} μs')
axes[0].set_xlim(26500,40000)
axes[1].set_xlim(34600,34610)
axes[1].set_ylim(0,3.5)
axes[0].legend()
axes[1].set_xlabel('Frequency (MHz)')
[19]:
Text(0.5, 0, 'Frequency (MHz)')
../../_images/python_notebooks_single-fid_35_1.png

A greater variety of window functions is available in the Python module compared with Blackchirp. The winf parameter can be set to any value supported by scipy.signal.get_window. For example, a generalized Gaussian window with variable p and σ parameters can be used by passing a tuple with the window name and parameter values:

[20]:
p = 1.5
sigma = len(fid)//5
x4,y4 = fid.ft(winf=('general_gaussian',p,sigma))

fig,axes = plt.subplots(2,1,figsize=(10,6))
for ax in axes:
    ax.plot(x,y,label='No window')
    ax.plot(x4,y4,label=f'Gaussian window (p={p:.2f},σ={int(sigma)})')
axes[0].set_xlim(26500,40000)
axes[1].set_xlim(34600,34610)
axes[1].set_ylim(0,3.5)
axes[0].legend()
axes[1].set_xlabel('Frequency (MHz)')
[20]:
Text(0.5, 0, 'Frequency (MHz)')
../../_images/python_notebooks_single-fid_37_1.png

Like the FID, the FT y array is 2-dimensional, where the second axis corresponds to the frame number. By default, the FT is applied to all frames at once; if only a single frame is desired, pass its index as the frame parameter.

[21]:
y.shape
[21]:
(375001, 1)
[ ]: