Drift across a time series#
A time series of the same specimen drifts: each frame sees it a little further along. quantem.gpu.drift measures how
far between the frames’ phase images and moves the frames back into register. It corrects translation only: no
scan distortion, no change of the scan positions.
from quantem.gpu.drift import drift
found = drift(phases, sampling_A=0.124) # phases: (frames, rows, cols)
found # table: each frame's displacement and σ, pairs rejected, closure
found.show(phases) # frame 0, the mean before and after correction, spectra, the path
aligned, mean = found.corrected(phases) # every frame on frame 0, over the region all frames share
phases can be a GPU tensor or CuPy array, a NumPy array (moved to the GPU), a list of 2D images, a list of SSB
results, or the SSB stream itself. A stream or result supplies its own sampling and a stream its frame names, so
drift(stream) needs no other argument. On a stream, stream.align() runs the same drift and also re-places the
stream’s own combined image with it; call drift(stream) for the Drift alone.
What the numbers mean#
found.displacement_A[f] is (row, col) in Å: where frame f sees the specimen against frame 0. Frame f’s image is
frame 0’s moved by it, I_f(r) = I_0(r - d_f); corrected reads I_f(r + d_f) by an exact Fourier shift and keeps the
rows and columns inside every frame. found.sigma_A[f] is its one-sigma uncertainty from the least-squares residuals.
attribute |
meaning |
|---|---|
|
(frames, 2) Å, frame 0 at zero |
|
(frames, 2) Å, one sigma |
|
every registered pair: shift, confidence, whether it was kept |
|
|
|
the frame labels (a stream’s names, else 0, 1, 2, …) |
Which registration#
call |
when |
how |
|---|---|---|
|
each frame images the specimen well (a bilayer at full dose) |
every pair up to 3 frames apart by its own lattice-safe search, a confident chain outvoting a pair that took a wrong lattice image, solved by least squares |
|
frames too sparse to place one against another |
each frame against the sum of all the others in register; no pairs, so no σ or closure |
Which reconstruction to register on#
It does not matter. On a 40-frame experimental MoS2 bilayer series the drift measured on SSB, phase-only and amplitude + phase agree to
0.05 Å over 116 Å of drift, the size of the drift’s own σ. Measure it on whichever you have (the stream gives SSB) and
correct any reconstruction of the same frames with it: found.corrected(phase_only_stack). The frames must share frame
0’s origin; a stack at another pixel size moves by its own pixels (corrected(stack, sampling_A=...), read from a
stream or results when they carry it).
Evidence: method comparison.
Advanced#
drift(..., frames=None, neighbours=3, band_A=(50.0, 0.67), reach_A=30.0, outlier_A=0.5): the frame labels (a
stream’s names, else 0, 1, 2, …), how many frames apart pairs are registered, the band of periods compared (Å), how
far a pair’s search reaches (Å), and how far a pair may disagree with the confident chain before it is rejected (Å).
running(..., passes=2): how many rounds each frame is re-placed against all the others.
register(reference, moving, sampling_A=...) registers one pair. Drift.plot(others) draws the path beside other
measures of it (an ADF drift, an earlier run).