Source code for sofia_redux.scan.chopper.chopper_numba_functions
# Licensed under a 3-clause BSD style license - see LICENSE.rst
import math
import numba as nb
import numpy as np
nb.config.THREADING_LAYER = 'threadsafe'
__all__ = ['find_transitions']
[docs]
@nb.njit(cache=True, nogil=False, parallel=False, fastmath=False)
def find_transitions(x, y, threshold): # pragma: no cover
"""
Given (x, y) chopper positions, find the transitions and other parameters.
Given a chopper position in (x, y) coordinates, centered about (0, 0),
return the index on (x, y) marking the `start` of the first transition,
the index marking the `end` of the last transition, the number of
`transitions`, the calculated `angle` between the two chop positions, and
the `distance` from the center while chopping.
This algorithm assumes that there are two chopping positions present in the
(x, y) data (on and off) that are relative to a central (0, 0) nominal chop
position. A chop transition is marked when the absolute x or y position
is greater than `tolerance` and of the opposite sign to the last detected
chop transition. Once a transition has been detected in both the x and
y directions, the distance will be calculated as:
d = sqrt(x^2 + y^2)
for each subsequent (x, y) position. The returned angle is weighted by
distance (d) from the center and is given by:
a = sum(d * arctan(s * y, s * x)) / sum(d)
where s is given by:
s = sign(y); x = 0
s = sign(x); otherwise
It is up to the user on how to best extract the chop amplitude from the
data given the chop profile. For a standard box-step chop profile, a
median on the distances should suffice, while more advanced analysis
should be applied to other patterns.
Parameters
----------
x : numpy.ndarray (float)
The x-direction chopper position of shape (n,).
y : numpy.ndarray (float)
The y-direction chopper position of shape (n,).
threshold : float
The distance away from the center (x, y) = (0, 0) that would be
considered representing a single chopper transition
(chopper amplitude).
Returns
-------
start, end, transitions, angle, distance : int, int, int, float, np.ndarray
"""
n = x.size
x_positive = False
y_positive = False
x_transitions = 0
y_transitions = 0
x_from = -1
y_from = -1
x_to = -1
y_to = -1
sum_a = 0.0
sum_w = 0.0
started = False
distance = np.empty(n, dtype=np.float64)
count = 0
start = 0
end = 0
transitions = 0
for i in range(n):
dx = x[i]
dy = y[i]
if not started:
x_positive = dx > 0.0
y_positive = dy > 0.0
started = True
continue
if ((x_positive and dx < threshold)
or (not x_positive and dx > threshold)):
x_positive = not x_positive
if x_transitions == 0:
x_from = i
else:
x_to = i
x_transitions += 1
if ((y_positive and dy < threshold)
or (not y_positive and dy > threshold)):
y_positive = not y_positive
if y_transitions == 0:
y_from = i
else:
y_to = i
y_transitions += 1
if x_transitions > 0 and y_transitions > 0:
d = math.hypot(dx, dy)
if d > threshold:
if dx == 0.0:
sign = -1 if dy < 0.0 else 1
else:
sign = -1 if dx < 0.0 else 1
sum_a += d * math.atan2(sign * dy, sign * dx)
sum_w += d
distance[count] = d
count += 1
if x_transitions > y_transitions:
start = x_from
end = x_to
transitions = x_transitions
else:
start = y_from
end = y_to
transitions = y_transitions
if sum_w > 0:
angle_radians = sum_a / sum_w
else:
angle_radians = 0.0
return start, end, transitions, angle_radians, distance[:count]