"""
:file: queso_cluster/atoms/base.py
:lang: python
:synopsis:
:author: Sarah Riley <academic@sriley.dev>
"""
import numpy as np
import dask.array as da
import numba as nb
from . import error as errAtom
from ..addon.logg import logger
import warnings
[docs]
def concatSpectra(dataSquareLst):
"""
Function to concatenate several channels of data into one array for clustering
Parameters
----------
dataSquareLst : dask.array
dask arrays containing spectral data to be concatenated
Returns
-------
dask.array
Concatenated spectral profiles
TODO
----
This feature needs to be properly tested
"""
return(da.concatenate(dataSquareLst))
[docs]
@nb.njit()
def numba_histogram(a, bins, lim):
"""
Numba accelerated histogram function
Parameters
----------
a : int
detail
bins : int
the number of bins in the histogram
lim : ndarray
the top and bottom of the histogram
Returns
-------
hist : ndarray
the histogram
bin_edges : ndarray
1D array of the bin edges
"""
hist = np.zeros((bins,), dtype=np.intp)
bin_edges = get_bin_edges(bins, lim)
for x in a.flat:
bin = compute_bin(x, bin_edges)
if bin is not None:
hist[int(bin)] += 1
return hist, bin_edges
[docs]
def rotateArray(image, turns):
"""
Rotates an array
Parameters
----------
image : ndarray
2D array to be rotated
turns : int
Number of pi/2 turns to rotate
Returns
-------
ndarray
Rotated array
"""
for i in range(turns):
image = np.array(list(zip(*image[::-1])))
return(image[::-1])
[docs]
@nb.njit(cache=True)
def get_bin_edges(bins, lim):
#> detail:
#> param type bins:
#> param type lim:
#> return (type):
#> test-method:
bin_edges = np.zeros((bins+1,), dtype=np.float64)
a_min = lim.min()
a_max = lim.max()
delta = (a_max - a_min) / bins
for i in range(bin_edges.shape[0]):
bin_edges[i] = a_min + i * delta
bin_edges[-1] = a_max # Avoid roundoff error on last point
return bin_edges
[docs]
@nb.njit()
def compute_bin(x, bin_edges):
#> detail:
#> param type x:
#> param type bin_edges:
#> return (type):
#> test-method:
# assuming uniform bins for now
n = bin_edges.shape[0] - 1
a_min = bin_edges[0]
a_max = bin_edges[-1]
# special case to mirror NumPy behavior for last bin
if x == a_max:
return n - 1 # a_max always in last bin
bin = int(n * (x - a_min) / (a_max - a_min))
if bin < 0 or bin >= n:
return None
else:
return bin
[docs]
@nb.njit
def np_gradient(f):
"""
Numba accelerated gradient
Paramters
---------
f : ndarray
1D array to take the gradient of
Returns
-------
ndarray
Gradient of f
"""
out = np.empty_like(f, np.float64)
out[1:-1] = (f[2:] - f[:-2]) / 2.0
out[0] = f[1] - f[0]
out[-1] = f[-1] - f[-2]
return(out)
[docs]
@nb.njit()
def minimize(data, decisions, size):
"""
"""
data_label = np.zeros(data.shape[0], dtype=np.uint32)
D_x = np.zeros(data.shape[0], dtype=data.dtype)
sq_dist = np.zeros(size, dtype=data.dtype)
for ii in range(data.shape[0]):
for kk in range(size):
sq_dist[kk] = similarityMetric(data[ii,:], decisions[kk, :])
data_label[ii] = sq_dist.argmin()
D_x[ii] = sq_dist[int(data_label[ii])]
return(data_label, D_x)
[docs]
@nb.njit(cache=True)
def maximize(data, decisions, size):
"""
"""
data_label = np.zeros(data.shape[0])
D_x = np.zeros(data.shape[0])
sq_dist = np.zeros(size)
for ii in range(data.shape[0]):
for kk in range(size):
sq_dist[kk] = similarityMetric(data[ii,:], decisions[kk, :])
data_label[ii] = sq_dist.argmax()
D_x[ii] = sq_dist[nb.u4(data_label[ii])]
return(data_label, D_x)
[docs]
@nb.njit(cache=True)
def np_all_axis0(x):
#> detail: Numba compatible version of np.all(x, axis=0)
#> param type x:
#> return (type):
#> test-method:
out = np.ones(x.shape[1], dtype=np.bool8)
for i in range(x.shape[0]):
out = np.logical_and(out, x[i, :])
return out
[docs]
@nb.njit(cache=True)
def np_all_axis1(x):
#> detail: Numba compatible version of np.all(x, axis=1)
#> param type x:
#> return (type):
#> test-method:
out = np.ones(x.shape[0], dtype=np.bool8)
for i in range(x.shape[1]):
out = np.logical_and(out, x[:, i])
return out
[docs]
@nb.njit()
def similarityMetric(x, y, type='dist', ref=0):
#> detail:
#> param type x:
#> param type y:
#> param type ['dist'] type:
#> param type [0] ref:
#> return (type):
#> test-method:
if type == 'dist':
delta = (x - y).astype(x.dtype)
metric = np.sqrt(delta.dot(delta))
elif type == 'cosine':
ref = 1.0 - 0.5 * np.exp(-11*np.arange(-int(len(x)/2), int(len(x)/2), step=1)**2)
x -= ref
y -= ref
similarityCoeff = (x).dot(y) / (np.sqrt(x.dot(x)) * np.sqrt(y.dot(y)))
metric = 1 - similarityCoeff
return(metric)
[docs]
@nb.njit()
def startMax(data, k, decisions):
"""
Heuristic k-means++.
Rather than selecting from a distribution around the furtherest datapoint, this initialization simply selects the furtherest datapoint as the next representative
Parameters
----------
data : ndarray
data pool for finding the initial representative profiles
k : int
The number of clusters
decisions : ndarray
Array containing the initial, randomly selected representative profile and empty slots for remaining profiles
Returns
-------
decisions : ndarray
Array containing a full set of representative profiles
"""
killer = np.ones(decisions.shape[1], dtype=decisions.dtype)
while True:
dc_left = np.flatnonzero(1-np_all_axis1(decisions))
if len(dc_left) == 0:
return(decisions)
print((k, len(dc_left)))
D_x = np.zeros(data.shape[0])
for ii in range(data.shape[0]):
for kk in range(k-len(dc_left)):
D_x[ii] += similarityMetric(data[ii,:], decisions[kk, :])
D_x /= (k - len(dc_left))
if (killer - data[D_x.argmax(), :]).sum() == 0:
return(decisions)
decisions[dc_left[0], :] = data[D_x.argmax(), :]
killer = data[D_x.argmax(), :]
[docs]
@nb.njit()
def startPlusPlus(data, k, decisions):
"""
k-means++ initialization
Parameters
----------
data : ndarray
data pool for finding the initial representative profiles
k : int
The number of clusters
decisions : ndarray
Array containing the initial, randomly selected representative profile and empty slots for remaining profiles
Returns
-------
decisions : ndarray
Array containing a full set of representative profiles
"""
killer = np.ones(decisions.shape[1], dtype=decisions.dtype)
while True:
dc_left = np.flatnonzero(1-np_all_axis1(decisions))
if len(dc_left) == 0:
return(decisions)
_, D_x = minimize(data, decisions, k-len(dc_left))
Dx2 = D_x**2 / (D_x**2).sum()
indx = np.searchsorted(np.cumsum(Dx2), np.random.rand(1))[0]
if (killer - data[indx, :]).sum() == 0:
return(decisions)
decisions[dc_left[0], :] = data[indx, :]
killer = data[indx, :]
@nb.njit(cache=True)
def _calcMoment(waveAxis, ii, jj, lineCore, dataCube, order, ref, counter=0):
#> detail:
#> param type waveAxis:
#> param type ii:
#> param type jj:
#> param type lineCore:
#> param type dataCube:
#> param type order:
#> param type ref:
#> param type [0] counter:
#> return (type):
#> test-method:
momentN = np.zeros((order+1, dataCube.shape[0]))
while counter <= order:
factor = np.power((waveAxis[ii:jj] - waveAxis[lineCore]), counter)
for i in range(dataCube.shape[0]):
momentN[counter, i] = ((dataCube[i, ii:jj] - ref[ii:jj])*factor).sum()
if counter > 0:
momentN[counter, :] /= momentN[0, :]
counter += 1
return(momentN)
# @nb.njit()
# def _calcFeatureDensity(data, converge, zindx, func1):
# #> detail:
# #> param type data:
# #> param type converge:
# #> param type zindx:
# #> param type func1:
# #> return (type):
# #> test-method:
# featureDensityArr = np.zeros(50)
# for a in range(featureDensityArr.shape[0]):
# scores1 = np.zeros(len(zindx))
# for i in range(int(scores1.shape[0]/100)):
# print((a, i, i*100/(len(zindx)/100)))
# labels = _runOptimization(i+1, data[zindx, :], converge)
# scores1_tmp = func1(data[zindx, :], labels)
# scores1[i] = scores1_tmp
# featureDensityArr[a] = np.min(np.where(scores1 == np.nanmax(scores1))[0]) + 1
# featureDensity = np.median(featureDensityArr)/len(zindx)
# return(featureDensity)
[docs]
@nb.njit()
def calcOptimization(k, data, decision, threshold=1e-6):
"""
Calculates the set of k decisions with a convergence threshold
Parameters
----------
k : int
The number of groups
data : ndarray
The data to be clustered
decision : ndarray
The previous iteration's decisions
threshold : float, optional
The convergence threshold
Raises
------
ConvergenceError
If the convergence criteria cannot be evaulated or if the convergence criterion is not met after :obj:`~queso_cluster.atoms.error.covergeLimit`
Returns
-------
decision : ndarray
The current iteration's representative profiles
data_label : ndarray
The labels for the data
"""
killCounter = 0
while True:
converge = -1
newCentroid = np.zeros((k, data.shape[1]), dtype=data.dtype)
dataLabel, _ = minimize(data, decision, k)
labelLst = np.unique(dataLabel)
if labelLst.size != k:
# print(labelLst)
# print(np.arange(k))
# print((data.shape, labelLst.size, k))
# print(decision.shape)
for qq in range(decision.shape[0]):
for rr in range(decision.shape[0]):
if rr != qq:
dd = similarityMetric(decision[rr,:], decision[qq, :])
print(dd)
raise errAtom.RPConflictWarning
for kk in range(labelLst.size):
#print(np.where(data_label == kk))
subData = data[np.where(dataLabel == labelLst[kk])[0], :]
newCentroid[kk,:] = subData.sum(axis=0)/subData.shape[0]
# new_centroid[kk,:] = np.median(sub_data, axis=0)
diffCentroid = newCentroid[kk,:] - decision[kk,:]
#print(np.asarray([converge, len(np.where(data_label == labelLst[kk])[0]),
# sub_data.shape[0], diff_centroid.dot(diff_centroid)]))
converge = np.max(np.asarray([converge,
np.sqrt(diffCentroid.dot(diffCentroid))]))
if not np.isfinite(converge):
raise errAtom.ConvergenceError(f"Converge criteria cannot be evaluated. Convergence is not finite")
if killCounter == errAtom.convergeLimit:
raise errAtom.ConvergenceError(f"Convergence condition not met after {killCounter} steps")
decision = newCentroid
if converge <= threshold:
return(decision, dataLabel)
killCounter += 1
[docs]
def labelGluer(labels):
#> detail:
#> param type labels:
#> return (type):
#> test-method:
time_label_concat = np.char.asarray(np.zeros(labels[0].shape[1], dtype=int))
for i in range(len(labels)):
for l in range(labels[i].shape[0]):
time_label_concat = np.char.add(time_label_concat, np.char.asarray(labels[i][l, ...].astype(int)))
return(time_label_concat)
[docs]
def labelReorder(labels):
#> detail:
#> param type labels:
#> return (type):
#> test-method:
time_label = []
for i in range(len(labels)):
time_label_wave = labelGluer([labels[i]])
time_label_lst = np.unique(time_label_wave)
time_label_bool = [element.decode("utf-8").find("-") < 0 for element in time_label_lst]
for j in range(len(time_label_lst)):
if time_label_bool[j]:
newLabel = int('9' * int(np.ceil(np.log10(len(time_label_lst))) + 1)) - j
while newLabel in np.unique(time_label_lst):
if not (newLabel % 10):
newLabel -= 1
time_label_wave[np.where(time_label_wave == time_label_lst[j])[0]] = newLabel
else:
time_label_wave[np.where(time_label_wave == time_label_lst[j])[0]] = -1
time_label.append(time_label_wave)
time_label_concat = np.char.asarray(np.zeros(labels[0].shape[1], dtype=int))
for i in range(len(labels)):
time_label_concat = np.char.add(time_label_concat, np.char.asarray(time_label[i]))
return(time_label_concat)
@nb.njit()
def _calcQuiescentFrame(spectralData, spectralParams, contIndxs, progress=None):
#> detail:
#> param type spectralData:
#> param type spectralParams:
#> param type contIndxs:
#> param type [None] progress:
#> return (type):
#> test-method:
lineCore, ii, jj = spectralParams
quiescentFrame = np.zeros(spectralData.shape[1:])
for x in range(spectralData.shape[1]):
for y in range(spectralData.shape[2]):
quiescentScanNum = np.zeros(spectralData.shape[0])
for t in range(spectralData.shape[0]):
quiescentScanNum[t] = spectralData[t, x, y, lineCore]/np.nanmax(spectralData[t, x, y, :])#).sum(axis=-1)
qindx = np.argsort(quiescentScanNum)[len(quiescentScanNum)//4]#np.where(quiescentScanNum == np.median(quiescentScanNum))#
quiescentFrame[x, y, :] = spectralData[qindx, x, y, :]
if progress != None:
progress.update(1)
return(quiescentFrame.reshape(spectralData.shape[1]*spectralData.shape[2], spectralData.shape[-1]))
@nb.njit()
def _calcDynamicFrame(spectralData, dynamicScanNum, progress=None, delta=0):
#> detail:
#> param type spectralData:
#> param type dynamicScanNum:
#> param type [None] progress:
#> param type [0] delta:
#> return (type):
#> test-method:
dynamicFrame = np.zeros(spectralData.shape[1:]) + np.nan
for x in range(spectralData.shape[1]):
for y in range(spectralData.shape[2]):
T = nb.uint(dynamicScanNum[x, y] + delta)
if (T < spectralData.shape[0]) and (T >= 0):
dynamicFrame[x, y, :] = spectralData[T, x, y, :]
if progress != None:
progress.update(1)
return(dynamicFrame)