Synchronizing individual laser log samples

· ☕ 5 min read · ✍️ Joe

Laser logs can save a lot of time when setting up selections in iolite. Rather than painstakingly reviewing each bit of data and assigning it to a selection/group, the time stamps in a laser log and the analysis annotations can be matched up with mass spec data to automate this process. Usually this works great. However, there are certain combinations of instruments that just cannot seem to agree on the timing, and when this happens it can be very frustrating as what should take seconds is now taking minutes or even hours!

When a laser log is synchronized in iolite using the normal procedure (i.e. via the import log button and subsequent dialog), the laser log in its entirety is synchronized with the data using an algorithm similar to the one below. That means if the laser computer and mass spec computer do not agree on the timing, the two may be not always be in sync, and how they differ may not be predictable. This results in automatic selections starting/ending before/after they should and, more importantly, inaccurate and/or imprecise results without manually intervention. An example is shown below where the laser log has been optimally synchronized with the data, but certain analyses do not line up.

problem

Synchronizing two signals

The task of synchronizing the laser log and the mass spec data boils down to working out the cross-correlation between the two. Usually, the laser log is represented by a square wave with ‘up’ and ‘down’ events corresponding to the laser ‘on’ and ‘off’ events recorded in the laser log, and the mass spec data is represented by the TotalBeam (sum of all input data).

There are, of course, many ways to accomplish this task in practice, one of which is included below (copied from here) and is very similar to the C++-based implementation used internally by iolite. This approach employs numpy‘s speedy fast Fourier transform (fft) routines and some fancy math to work out the time offset between the two signals much faster than a more verbose and non-mathematician-readable implementation (e.g. using numpy’s correlate function).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import numpy as np
from numpy.fft import fft, ifft, fft2, ifft2, fftshift

def cross_correlation_using_fft(x, y):
    f1 = fft(x)
    f2 = fft(np.flipud(y))
    cc = np.real(ifft(f1 * f2))
    return fftshift(cc)

# shift < 0 means that y starts 'shift' time steps before x 
# shift > 0 means that y starts 'shift' time steps after x
def compute_shift(x, y):
    assert len(x) == len(y)
    c = cross_correlation_using_fft(x, y)
    assert len(c) == len(x)
    zero_index = int(len(x) / 2) - 1
    shift = zero_index - np.argmax(c)
    return shift

For those interested in experimenting with synchronization in iolite, the above bit of python code is a good starting point. If instead you do not care about the synchronization details, but just want to get it done, you can use a helper function included as part of iolite’s python API:

1
2
3
4
# with:
# x1, y1 = the time, data arrays of the first signal
# x2, y2 = the time, data arrays of the second signal
shift = data.timeOffset(x1, y1, x2, y2)

Getting and manipulating the required data

iolite’s python API (introduced here and mostly documented here) provides everything we need to get the required data, synchronize, and update the laser log samples. Note: setting laser log sample start/end times via python is only supported in iolite v.4.3.10+.

Channel time and data can be access as:

1
2
3
4
5
t = data.timeSeries('TotalBeam').time()
# Don't do this:
# data = data.timeSeries('TotalBeam').data()
# because you'll overwrite the interface to iolite's data
d = data.timeSeries('TotalBeam').data()

Laser log samples can be accessed and manipulated from iolite’s python API as follows:

1
2
3
4
5
6
7
samples = data.laserLogSamples() # a list of SampleMetadataPyInterface objects

st = samples[0].startTime() # start time of first sample as QDateTime type
et = samples[-1].endTime() # end time of last sample as QDateTime type

samples[0].setStartTime(st.addMSecs(5e3)) # set start time to 5 seconds later
samples[0].setEndTime(st.addMSecs(-5e3)) # set end time to 5 seconds earlier 

The methods available for QDateTime objects can be found here.

Synchronizing individual laser log samples

Putting it all together, we can optimize the synchronization of each individual laser log sample locally using a script as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np

tb = data.timeSeries('TotalBeam')
sg = data.createSelectionGroup('LocalAdjustTemp', data.Sample)
ts = np.median(np.diff(tb.time()))
n = int(round(10/ts)) # 10 = number of seconds to search around log sample

for i, lls in enumerate(data.laserLogSamples()):
    sel = data.createSelection(sg, lls.startTime(), lls.endTime(), 'Temp')
    si = tb.selectionIndices(sel)
    try:
        da = tb.data()[si[0]-n:si[-1]+n] # Take n points before or after sample
        lla = np.pad(np.ones(si[-1] - si[0]), (n, n), 'constant', constant_values=(0, 0))
        offset = data.timeOffset(np.arange(len(da)), da, np.arange(len(lla)), lla)
    except Exception as e:
        print('problem for %i, %s'%(i, e))
        continue

    print('%i = %i, %f s, len(da) = %i, len(lla) = %i'%(i, offset, offset*ts, len(da), len(lla)))
    lls.setStartTime(lls.startTime().addMSecs(offset*ts*1000))
    lls.setEndTime(lls.endTime().addMSecs(offset*ts*1000))

data.removeSelectionGroup('LocalAdjustTemp')

Which, for the problem illustrated at the beginning of this note for OGC-3, produces an adjusted laser log sample as shown below. New selections created from the laser log samples will now have improved synchronization.

sync

And that’s it!

Click here to discuss.


iolite team
WRITTEN BY
Joe
iolite developer


What's on this Page