# 
#   This file is part of Mantis, a Multivariate ANalysis Tool for Spectromicroscopy.
# 
#   Copyright (C) 2011 Mirna Lerotic, 2nd Look
#   http://2ndlookconsulting.com
#   License: GNU GPL v3
#
#   Mantis is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   any later version.
#
#   Mantis is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details <http://www.gnu.org/licenses/>.


from __future__ import division

import os
import copy
import numpy as np
import scipy.interpolate
import scipy.spatial
import scipy.ndimage
from scipy.cluster.vq import  kmeans2, whiten
import scipy.signal
from .file_plugins import file_stk


#------------------------------------------------------------------------------
def erf(x):
    # save the sign of x
    sign = 1
    if x < 0: 
        sign = -1
    x = abs(x)

    # constants
    a1 =  0.254829592
    a2 = -0.284496736
    a3 =  1.421413741
    a4 = -1.453152027
    a5 =  1.061405429
    p  =  0.3275911

    # A&S formula 7.1.26
    t = 1.0/(1.0 + p*x)
    y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*np.exp(-x*x)
    return sign*y # erf(-x) = -erf(x)

def stepfunc(p, x):
    #JS Nexafs book - step function
    # P - position of the inflection point
    # H - step height
    # G - FWHM width of the step
    # E - independent variable, energy (x)
    
    P = p[0]
    H = p[1]
    G = p[2]
    
    c = 1.665
    
    y = H*(0.5+0.5*erf((x-P)/(G/c)))

    return y

def gaussian(p, x):
    
    A = p[0]
    mu = p[1]
    sigma = p[2]
    #offset = p[3]
    
    y = A * np.exp(-((x-mu)**2)/(2*sigma**2))#+offset
    
    return y

def model(p, nsteps, npeaks, x):
    
    offset = p[0]
    
    pg = nsteps*3+1
    
    istepfitparams = p[1:pg]
    
    y = np.zeros((x.size))
       
    if nsteps > 0:
        for i in range(x.size):
            y[i] = stepfunc(istepfitparams, x[i])

    for i in range(npeaks):
        pp = [p[pg+i*3],p[pg+1+i*3],p[pg+2+i*3]]
        y = y + gaussian(pp,x)
        
    y = y+offset
    
    return y 


def model_error(p, nsteps, npeaks, x, y):

#    err = np.zeros(x.size)
#     for i in range(x.size):
#         err[i] = (y[i]-model(p, x[i]))
#     return err

    err = y-model(p, nsteps, npeaks, x)
    
    return err


#-----------------------------------------------------------------------
class Cfitparams:
    def __init__(self):
       
        self.base = 0.0
        self.stepfitparams = np.zeros((8))
        self.gauss_fp_a = np.zeros((12))
        self.gauss_fp_m = np.zeros((12))
        self.gauss_fp_s = np.zeros((12))
        
        

#-----------------------------------------------------------------------
class analyze:
    def __init__(self, stkdata):
        self.stack = stkdata
        
        self.pca_calculated = 0
        self.clusters_calculated = 0
        
       
        self.target_spectra = 0
        self.tspectrum_loaded = 0
        self.n_target_spectra = 0
        self.tspec_names = []
        
        self.xrayfitsp_loaded = 0
        self.xrayfitspectra = 0
        self.n_xrayfitsp = 0
        self.xfspec_names = []
        self.xfitpars = []
        
        self.pcaimages4D = []
        self.eigenvals4D = []
        self.eigenvecs4D = []
        self.variance4D = []
        self.pcaimagebounds4D = []
        
        
        self.target_svd_maps4D = []
        self.original_svd_maps4D = []
        self.target_pcafit_maps4D = []
        self.original_fit_maps4D = []
        self.target_pcafit_coeffs4D = []
        self.target_pcafit_spectra4D = []

        
        
#-----------------------------------------------------------------------   
# Calculate pca 
    def delete_data(self):
        
        self.target_spectra = 0
        self.tspectrum_loaded = 0
        self.n_target_spectra = 0
        self.tspec_names = []
        
        self.pcaimages = 0
        self.pcaimagebounds = 0
        self.eigenvals = 0
        self.eigenvecs = 0
        
        self.cluster_distances = 0
        self.clustersizes = 0
        self.cluster_indices = 0
        self.clusterspectra = 0
                    
#-----------------------------------------------------------------------   
# Calculate pca 
    def calculate_pca(self):
        #covariance matrix
        n_pix = self.stack.n_cols*self.stack.n_rows

        od = self.stack.od
        
        #normalize od spectra - not used in pca_gui.pro
        #norms = np.apply_along_axis(np.linalg.norm, 1, od)
        odn = np.zeros((n_pix, self.stack.n_ev))
        for i in range(n_pix):
            odn[i,:] = od[i,:]/np.linalg.norm(od[i,:])
            
       
        covmatrix = np.dot(od.T,od)

        self.pcaimages = np.zeros((self.stack.n_cols, self.stack.n_rows, self.stack.n_ev))
        self.pcaimagebounds = np.zeros((self.stack.n_ev))

        try:

            self.eigenvals, self.eigenvecs = np.linalg.eigh(covmatrix)

            #sort the eigenvals and eigenvecs       
            perm = np.argsort(-np.abs(self.eigenvals))
            self.eigenvals = self.eigenvals[perm]
            self.eigenvecs = self.eigenvecs[:,perm]

            self.pcaimages = np.dot(od,self.eigenvecs)

            #calculate eigenimages
            self.pcaimages = np.reshape(self.pcaimages, (self.stack.n_cols, self.stack.n_rows, self.stack.n_ev), order='F')

            #Find bounds for displaying color-tables
            for i in range(self.stack.n_ev):
                min_val = np.amin(self.pcaimages[:,:,i])
                max_val = np.amax(self.pcaimages[:,:,i])
                self.pcaimagebounds[i] = np.amax((np.abs(min_val), np.abs(max_val)))

            
            #calculate variance captured by the pca components
            self.variance = self.eigenvals.copy()
            
            totalvar = self.variance.sum()
            
            self.variance = self.variance/totalvar
            
            #Scree plot - find an elbow in the curve - between 1 and 20 components
            maxpoints = min(25, self.stack.n_ev-1)
            #Find a line between first (x1, y1) and last point (x2, y2) and calculate distances:
            y2 = np.log(self.eigenvals[maxpoints])
            x2 = maxpoints
            y1 = np.log(self.eigenvals[0])
            x1 = 0

            #Calculate distances between all the points and the line x1 and x2 are points on the line and x0 are eigenvals
            distance = np.zeros((maxpoints))
            for i in range(maxpoints):
                y0 = np.log(self.eigenvals[i])
                x0=i
                distance[i] = np.abs((x2-x1)*(y1-y0)-(x1-x0)*(y2-y1))/np.sqrt((x2-x1)**2+(y2-y1)**2)
            #Point with the largest distance is the "elbow"
            sigpca = np.argmax(distance)

            self.numsigpca = sigpca + 1
            

                        
        except:
            print( "pca not converging")
            
        self.pca_calculated = 1
        
        if self.n_target_spectra > 1:
            self.fit_target_spectra()

        return    

#-----------------------------------------------------------------------   
# Calculate pca 
    def calculate_pca_4D(self):
        #covariance matrix
        n_pix = self.stack.n_cols*self.stack.n_rows
        
        self.pcaimages4D = []
        self.eigenvals4D = []
        self.eigenvecs4D = []
        self.variance4D = []
        self.pcaimagebounds4D = []
        
        for jth in range(self.stack.n_theta):
            
            od3d = self.stack.od4D[:,:,:,jth]
            od = od3d.copy()
            
            od = np.reshape(od, (n_pix, self.stack.n_ev), order='F')               

    
            
            #normalize od spectra - not used in pca_gui.pro
            #norms = np.apply_along_axis(np.linalg.norm, 1, od)
            odn = np.zeros((n_pix, self.stack.n_ev))
            for i in range(n_pix):
                odn[i,:] = od[i,:]/np.linalg.norm(od[i,:])
                
           
            covmatrix = np.dot(od.T,od)
      
            self.pcaimages = np.zeros((self.stack.n_cols, self.stack.n_rows, self.stack.n_ev))
            self.pcaimagebounds = np.zeros((self.stack.n_ev))
            
    
            try:
    
                self.eigenvals, self.eigenvecs = np.linalg.eigh(covmatrix)
    
                #sort the eigenvals and eigenvecs       
                perm = np.argsort(-np.abs(self.eigenvals))
                self.eigenvals = self.eigenvals[perm]
                self.eigenvecs = self.eigenvecs[:,perm]
                
                self.pcaimages = np.dot(od,self.eigenvecs)
    
                #calculate eigenimages
                self.pcaimages = np.reshape(self.pcaimages, (self.stack.n_cols, self.stack.n_rows, self.stack.n_ev), order='F')
    
                #Find bounds for displaying color-tables
                for i in range(self.stack.n_ev):
                    min_val = np.amin(self.pcaimages[:,:,i])
                    max_val = np.amax(self.pcaimages[:,:,i])
                    self.pcaimagebounds[i] = np.amax((np.abs(min_val), np.abs(max_val)))
    
                
                #calculate variance captured by the pca components
                self.variance = self.eigenvals.copy()
                
                totalvar = self.variance.sum()
                
                self.variance = self.variance/totalvar
                
                #Scree plot - find an elbow in the curve - between 1 and 20 components
                maxpoints = min(25, self.stack.n_ev-1)
                #Find a line between first (x1, y1) and last point (x2, y2) and calculate distances:
                y2 = np.log(self.eigenvals[maxpoints])
                x2 = maxpoints
                y1 = np.log(self.eigenvals[0])
                x1 = 0
                
                #Calculate distances between all the points and the line x1 and x2 are points on the line and x0 are eigenvals
                distance = np.zeros((maxpoints))
                for i in range(maxpoints):
                    y0 = np.log(self.eigenvals[i])
                    x0=i
                    distance[i] = np.abs((x2-x1)*(y1-y0)-(x1-x0)*(y2-y1))/np.sqrt((x2-x1)**2+(y2-y1)**2)
                
                #Point with the largest distance is the "elbow"
                sigpca = np.argmax(distance)
    
                self.numsigpca = sigpca + 1
                
    
                            
            except:
                print("pca not converging")
                
            self.pca_calculated = 1
            
            if self.n_target_spectra > 1:
                self.fit_target_spectra()
                
                
            self.pcaimages4D.append(self.pcaimages)
            self.eigenvals4D.append(self.eigenvals)
            self.eigenvecs4D.append(self.eigenvecs)
            self.variance4D.append(self.variance)
            self.pcaimagebounds4D.append(self.pcaimagebounds)

        return  
    
#----------------------------------------------------------------------- 
# Move PC up
    def move_pc_up(self, ipc):
        
        if ipc == 0:
            return

        if len(self.pcaimages4D) == 0:
            temp = self.pcaimages.copy()
            self.pcaimages[:,:, ipc] = temp[:,:, ipc-1]
            self.pcaimages[:,:, ipc-1] = temp[:,:, ipc]
            
            temp = self.pcaimagebounds.copy()
            self.pcaimagebounds[ipc] = temp[ipc-1]
            self.pcaimagebounds[ipc-1] = temp[ipc]
            
            temp = self.eigenvals.copy()
            self.eigenvals[ipc] = temp[ipc-1]
            self.eigenvals[ipc-1] = temp[ipc]        
            
            temp = self.eigenvecs.copy()
            self.eigenvecs[:, ipc] = temp[:, ipc-1]
            self.eigenvecs[:, ipc-1] = temp[:, ipc]
                    
            temp = self.variance.copy()
            self.variance[ipc] = temp[ipc-1]
            self.variance[ipc-1] = temp[ipc]     
        
        else:        
            
            for jth in range(self.stack.n_theta):
                
                temp = self.pcaimages4D[jth].copy()
                self.pcaimages4D[jth][:,:, ipc] = temp[:,:, ipc-1]
                self.pcaimages4D[jth][:,:, ipc-1] = temp[:,:, ipc]
                               
                temp = self.pcaimagebounds4D[jth].copy()
                self.pcaimagebounds4D[jth][ipc] = temp[ipc-1]
                self.pcaimagebounds4D[jth][ipc-1] = temp[ipc]
                
                temp = self.eigenvals4D[jth].copy()            
                self.eigenvals4D[jth][ipc] = temp[ipc-1]
                self.eigenvals4D[jth][ipc-1] = temp[ipc]        
                
                temp = self.eigenvecs4D[jth].copy()
                self.eigenvecs4D[jth][:, ipc] = temp[:, ipc-1]
                self.eigenvecs4D[jth][:, ipc-1] = temp[:, ipc]
                
                temp = self.variance4D[jth].copy()        
                self.variance4D[jth][ipc] = temp[ipc-1]
                self.variance4D[jth][ipc-1] = temp[ipc]                    
        
              
            
        if self.n_target_spectra > 1:
            self.fit_target_spectra()
            
            if len(self.target_svd_maps4D) > 0:
                self.calculate_targetmaps_4D()
                    

#-----------------------------------------------------------------------   
# Find clusters 
    def calculate_clusters(self, nclusters, remove1stpca = 0, sigmasplit = 0, pcscalingfactor = 0.0):
        #Reduced data matrix od_reduced(n_pixels,n_significant_components)
        #od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows, self.numsigpca))
       
        self.nclusters = nclusters
               
        npixels = self.stack.n_cols * self.stack.n_rows
       
        inverse_n_pixels = 1./float(npixels)
        inverse_n_pixels_less_one =  1./float(npixels-1)
        
        dc_offsets = np.zeros((self.numsigpca))
        #rms_deviations = np.zeros((self.numsigpca))
        od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows,self.numsigpca))
       
        for i in range(self.numsigpca):

            eimage = self.pcaimages[:,:,i]

            dc_offsets[i] = np.sum(eimage)*inverse_n_pixels
            # Since we're looking at deviations from an average,
            # we divide by (N-1).
            #rms_deviations[i] = np.sqrt(np.sum((eimage-dc_offsets[i])**2)*inverse_n_pixels_less_one)

            # The straightforward thing is to do
            #   d_reduced[i,0:(n_pixels-1)] = eimage
            # However, things work much better if we subtract the
            # DC offsets from each eigenimage.  One could also divide
            # by rms_deviations, but that seems to overweight
            # the sensitivity to weaker components too much.    
            rms_gamma = pcscalingfactor
            od_reduced[:,:,i] = (eimage-dc_offsets[i]) *(self.eigenvals[0]/self.eigenvals[i])**rms_gamma

       
    
        if remove1stpca == 0 :
            #od_reduced = od_reduced[:,:,0:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca), order='F')
        else:
            od_reduced = od_reduced[:,:,1:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca-1), order='F')
       

        indx = np.zeros(npixels)

        clustercentroids, indx = kmeans2(od_reduced, nclusters, iter=200, minit = 'points' )
        
       
        #calculate cluster distances
        self.cluster_distances = np.zeros((self.stack.n_cols*self.stack.n_rows))
        for i in range(npixels):
            clind = indx[i]
            self.cluster_distances[i] = scipy.spatial.distance.euclidean(od_reduced[i,:],clustercentroids[clind,:])          
           
        self.cluster_distances = np.reshape(self.cluster_distances, (self.stack.n_cols, self.stack.n_rows), order='F')
                    
     
        indx = np.reshape(indx, (self.stack.n_cols, self.stack.n_rows), order='F')
        self.clustersizes = np.zeros((nclusters,), dtype=int)
       
        for i in range(nclusters):
            clind = np.where(indx == i)
            self.clustersizes[i] = indx[clind].shape[0]
                   
        #sort the data with the cluster with the most members first  
        count_indices = np.argsort(self.clustersizes)
        count_indices = count_indices[::-1]
               
        self.cluster_indices = np.zeros((self.stack.n_cols, self.stack.n_rows), dtype=int)
             
        self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
               
        for i in range(nclusters):
            clind = np.where(indx == count_indices[i])
            self.cluster_indices[clind] = i
            self.clustersizes[i] = self.cluster_indices[clind].shape[0]

            for ie in range(self.stack.n_ev):  
                thiseng_od = self.stack.od3d[:,:,ie]
                self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
     
        #Calculate SSE Sum of Squared errors
        indx = np.reshape(self.cluster_indices, (npixels), order='F')
        self.sse = np.zeros((npixels))
        for i in range(npixels):
            clind = indx[i]
            self.sse[i] = np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:]))         
           
        self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        


        if (sigmasplit ==1):
            #Check the validity of cluster analysis and if needed add another cluster
            new_cluster_indices = self.cluster_indices.copy()
            new_nclusters = nclusters
            recalc_clusters = False
            
            for i in range(nclusters):
                clind = np.where(self.cluster_indices == i)
                cl_sse_mean = np.mean(self.sse[clind])
                cl_see_std = np.std(self.sse[clind])

                sigma9 = cl_sse_mean+9*cl_see_std
                maxsse = np.max(self.sse[clind])
                if (maxsse > sigma9): 
                    recalc_clusters = True
                    sse_helper = np.zeros((self.stack.n_cols, self.stack.n_rows), dtype=int)
                    sse_helper[clind] = self.sse[clind]
                    newcluster_ind = np.where(sse_helper > sigma9)
                    new_cluster_indices[newcluster_ind] = new_nclusters
                    new_nclusters += 1
                            
            
            if recalc_clusters == True:
                nclusters = new_nclusters
                self.cluster_indices = new_cluster_indices
                self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
                self.clustersizes = np.zeros((nclusters,), dtype=int)
                for i in range(nclusters):
                    clind = np.where(self.cluster_indices == i)
                    self.clustersizes[i] = self.cluster_indices[clind].shape[0]
                    if self.clustersizes[i]>0:
                        for ie in range(self.stack.n_ev):  
                            thiseng_od = self.stack.od3d[:,:,ie]
                            self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
             
                #Calculate SSE Sum of Squared errors
                indx = np.reshape(self.cluster_indices, (npixels), order='F')
                self.sse = np.zeros((npixels))
                for i in range(npixels):
                    clind = indx[i]
                    self.sse[i] = np.sqrt(np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:])))         
                   
                self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        
        
        self.cluster_distances = self.sse
        
        self.clusters_calculated = 1
        
        return int(nclusters)
        
#-----------------------------------------------------------------------   
# Find clusters 
    def calculate_clusters_4D(self, nclusters, remove1stpca = 0, sigmasplit = 0, pcscalingfactor = 0.0):
        #Reduced data matrix od_reduced(n_pixels,n_significant_components)
        #od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows, self.numsigpca))
       
        self.nclusters = nclusters
               
        npixels = self.stack.n_cols * self.stack.n_rows
       
        inverse_n_pixels = 1./float(npixels)
        inverse_n_pixels_less_one =  1./float(npixels-1)
        
        dc_offsets = np.zeros((self.numsigpca))
        #rms_deviations = np.zeros((self.numsigpca))
        od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows,self.numsigpca))
       
        for i in range(self.numsigpca):

            eimage = self.pcaimages[:,:,i]

            dc_offsets[i] = np.sum(eimage)*inverse_n_pixels
            # Since we're looking at deviations from an average,
            # we divide by (N-1).
            #rms_deviations[i] = np.sqrt(np.sum((eimage-dc_offsets[i])**2)*inverse_n_pixels_less_one)

            # The straightforward thing is to do
            #   d_reduced[i,0:(n_pixels-1)] = eimage
            # However, things work much better if we subtract the
            # DC offsets from each eigenimage.  One could also divide
            # by rms_deviations, but that seems to overweight
            # the sensitivity to weaker components too much.    
            rms_gamma = pcscalingfactor
            od_reduced[:,:,i] = (eimage-dc_offsets[i]) *(self.eigenvals[0]/self.eigenvals[i])**rms_gamma

       
    
        if remove1stpca == 0 :
            #od_reduced = od_reduced[:,:,0:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca), order='F')
        else:
            od_reduced = od_reduced[:,:,1:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca-1), order='F')
       

        indx = np.zeros(npixels)

        clustercentroids, indx = kmeans2(od_reduced, nclusters, iter=200, minit = 'points' )
        
       
        #calculate cluster distances
        self.cluster_distances = np.zeros((self.stack.n_cols*self.stack.n_rows))
        for i in range(npixels):
            clind = indx[i]
            self.cluster_distances[i] = scipy.spatial.distance.euclidean(od_reduced[i,:],clustercentroids[clind,:])          
           
        self.cluster_distances = np.reshape(self.cluster_distances, (self.stack.n_cols, self.stack.n_rows), order='F')
                    
     
        indx = np.reshape(indx, (self.stack.n_cols, self.stack.n_rows), order='F')
        self.clustersizes = np.zeros((nclusters,), dtype=int)
       
        for i in range(nclusters):
            clind = np.where(indx == i)
            self.clustersizes[i] = indx[clind].shape[0]
                   
        #sort the data with the cluster with the most members first  
        count_indices = np.argsort(self.clustersizes)
        count_indices = count_indices[::-1]
               
        self.cluster_indices = np.zeros((self.stack.n_cols, self.stack.n_rows), dtype=int)
             
        self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
               
        for i in range(nclusters):
            clind = np.where(indx == count_indices[i])
            self.cluster_indices[clind] = i
            self.clustersizes[i] = self.cluster_indices[clind].shape[0]

            for ie in range(self.stack.n_ev):  
                thiseng_od = self.stack.od3d[:,:,ie]
                self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
     
        #Calculate SSE Sum of Squared errors
        indx = np.reshape(self.cluster_indices, (npixels), order='F')
        self.sse = np.zeros((npixels))
        for i in range(npixels):
            clind = indx[i]
            self.sse[i] = np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:]))         
           
        self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        


        if (sigmasplit ==1):
            #Check the validity of cluster analysis and if needed add another cluster
            new_cluster_indices = self.cluster_indices.copy()
            new_nclusters = nclusters
            recalc_clusters = False
            
            for i in range(nclusters):
                clind = np.where(self.cluster_indices == i)
                cl_sse_mean = np.mean(self.sse[clind])
                cl_see_std = np.std(self.sse[clind])

                sigma9 = cl_sse_mean+9*cl_see_std
                maxsse = np.max(self.sse[clind])
                if (maxsse > sigma9): 
                    recalc_clusters = True
                    sse_helper = np.zeros((self.stack.n_cols, self.stack.n_rows), dtype=int)
                    sse_helper[clind] = self.sse[clind]
                    newcluster_ind = np.where(sse_helper > sigma9)
                    new_cluster_indices[newcluster_ind] = new_nclusters
                    new_nclusters += 1
                            
            
            if recalc_clusters == True:
                nclusters = new_nclusters
                self.cluster_indices = new_cluster_indices
                self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
                self.clustersizes = np.zeros((nclusters,), dtype=int)
                for i in range(nclusters):
                    clind = np.where(self.cluster_indices == i)
                    self.clustersizes[i] = self.cluster_indices[clind].shape[0]
                    if self.clustersizes[i]>0:
                        for ie in range(self.stack.n_ev):  
                            thiseng_od = self.stack.od3d[:,:,ie]
                            self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
             
                #Calculate SSE Sum of Squared errors
                indx = np.reshape(self.cluster_indices, (npixels), order='F')
                self.sse = np.zeros((npixels))
                for i in range(npixels):
                    clind = indx[i]
                    self.sse[i] = np.sqrt(np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:])))         
                   
                self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        
        
        self.cluster_distances = self.sse
        
        self.clusters_calculated = 1
        
        return int(nclusters)
       
#-----------------------------------------------------------------------   
# Find clusters 
    def calculate_clusters_kmeansangle(self, nclusters, remove1stpca = 0, sigmasplit = 0, 
                                       cosinemeasure = False):
        
        cosinemeasure = True
       
        self.nclusters = nclusters
               
        npixels = self.stack.n_cols * self.stack.n_rows
       
        inverse_n_pixels = 1./float(npixels)
        inverse_n_pixels_less_one =  1./float(npixels-1)
        
        dc_offsets = np.zeros((self.numsigpca))
        #rms_deviations = np.zeros((self.numsigpca))
        od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows,self.numsigpca))
       
        for i in range(self.numsigpca):

            eimage = self.pcaimages[:,:,i]

            dc_offsets[i] = np.sum(eimage)*inverse_n_pixels
            # Since we're looking at deviations from an average,
            # we divide by (N-1).
            #rms_deviations[i] = np.sqrt(np.sum((eimage-dc_offsets[i])**2)*inverse_n_pixels_less_one)

            # The straightforward thing is to do
            #   d_reduced[i,0:(n_pixels-1)] = eimage
            # However, things work much better if we subtract the
            # DC offsets from each eigenimage.  One could also divide
            # by rms_deviations, but that seems to overweight
            # the sensitivity to weaker components too much.    
            rms_gamma = 0.0
            od_reduced[:,:,i] = (eimage-dc_offsets[i]) *(self.eigenvals[0]/self.eigenvals[i])**rms_gamma

       
    
        if remove1stpca == 0 :
            #od_reduced = od_reduced[:,:,0:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca), order='F')
            nsigpca = self.numsigpca
        else:
            od_reduced = od_reduced[:,:,1:self.numsigpca]
            od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca-1), order='F')
            nsigpca = self.numsigpca-1
       
       
        
        n_iterations = 5
     
        # When "Angle distance measure" is used we d_reduced is normalized
        # so that all spectra have norm equal to 1 (this amounts to
        # projection of all the pixels to unit sphere in principal
        # component space.
        # For angle distance measure we will only include part of the
        # pixels if cutoff is set. Pixels with lowest optical density are
        # not included in the calculation since they will be uniformly
        # distributed over the unit sphere and might obstract finding of
        # the clusters.
        angle_cutoff_value = 0
        if cosinemeasure:
            od_reduced_old = od_reduced.copy()
            for i in range(npixels):
                od_reduced[i,:] = od_reduced[i,:]/np.linalg.norm(od_reduced[i,:])
                 
            if angle_cutoff_value > 0:
                d_integrated = np.zeros((npixels))
                d_integrated = np.apply_along_axis(np.sum, self.od, 0) 
                included_pixels = np.where(d_integrated > angle_cutoff_value)
         
                od_reduced_all_pixels = od_reduced.copy()
                od_reduced = od_reduced[:, included_pixels]
     
        

        # Initial 'learning rate'.
        LearningRates = 0.3-0.2*np.arange(n_iterations+1)/float(n_iterations-1)
        
        

        # Normal random cluster weights.
        cluster_weights = np.zeros((nsigpca, nclusters))
        randomindices = np.random.uniform(0, npixels-1,size=nclusters)
        for i in range(nclusters):
            cluster_weights[:,i] = od_reduced[randomindices[i], :]


        if cosinemeasure:
            for i in range(nclusters):
                cluster_weights[:, i] = cluster_weights[:, i]/np.linalg.norm(cluster_weights[:,i])



        Metric = np.zeros((nclusters))

        # Start by picking a percentage of the pixels at random,
        # and using them to start finding our cluster centers
        n_random_pixels = int(0.50*float(npixels))
        random_sample_indices = float(npixels-1)*np.random.uniform(0,1,(n_random_pixels,))
        # Make sure we don't do any pixels twice
        ursi, uindices = np.unique(random_sample_indices, return_index = True)
        random_sample_indices = random_sample_indices[uindices]
        n_random_pixels = len(random_sample_indices)
        this_learning_rate = LearningRates[0]
        for i_sample in range(n_random_pixels):
            Sample = random_sample_indices[i_sample]
            Vector = np.tile(od_reduced[Sample,:], (nclusters,1)).T - cluster_weights
            #Calculate distances
            if cosinemeasure == False:
                for i_cluster in range(nclusters):                    
                    Metric[i_cluster] = np.sqrt(np.dot(Vector[:, i_cluster].T, Vector[:, i_cluster]))
                   
            else:
                #Use angle between vectors instead euclidean distance
                for i_cluster in range(nclusters):
                    Metric[i_cluster] = scipy.spatial.distance.cosine(od_reduced[ Sample, :], cluster_weights[:, i_cluster].T)
                       
 
            MinIndex = np.argmin(Metric)
            cluster_weights[:,MinIndex] = this_learning_rate* Vector[:,MinIndex] + cluster_weights[:,MinIndex]
     
            if cosinemeasure:
                cluster_weights[:, MinIndex] = cluster_weights[:, MinIndex]/np.linalg.norm(cluster_weights[:,MinIndex])
                 
         
        # Random ordering of sample indices.
        random_ordered = np.arange(npixels)
        np.random.shuffle(random_ordered)
    
        self.cluster_distances = np.zeros((npixels))
        Tempcluster_indices = np.zeros((npixels))
        cluster_histogram = np.zeros((nclusters))
        cluster_indices = np.zeros((npixels))
        
        
        New_RMSDistanceIterations = np.zeros((n_iterations))
        for i_iteration in range(n_iterations):
            this_max_distance = 0.0
            this_learning_rate = LearningRates[i_iteration]

            for Sample in range(npixels):
                # In our case the data array is
                # d_reduced(n_significant_components,n_pixels), and we have
                # WorkCol(1,n_clusters).  Calculate
                # Vector(n_significant_components,n_clusters) by multiplying
                # all the components for this pixel by n_clusters values of
                # 1 to pick them off, and then subtracting from the result
                # the current guess of the weights (the cluster centers).
                Vector = np.tile(od_reduced[random_ordered[Sample],:], (nclusters,1)).T - cluster_weights

                #Calculate distances
                if cosinemeasure == False:
                    for i_cluster in range(nclusters):                    
                        Metric[i_cluster] = np.sqrt(np.dot(Vector[:, i_cluster].T, Vector[:, i_cluster].T))
                      
                else:
                    #Use angle between vectors instead euclidean distance
                    for i_cluster in range(nclusters):
                        Metric[i_cluster] = scipy.spatial.distance.cosine(od_reduced[random_ordered[Sample], :], cluster_weights[:, i_cluster].T)
         

                MinIndex = np.argmin(Metric)
                
                MinMetric = Metric[MinIndex]
                this_max_distance = max([this_max_distance, MinMetric ])
                cluster_weights[:,MinIndex] = this_learning_rate* Vector[:,MinIndex] + cluster_weights[:,MinIndex]
        
                if cosinemeasure:
                    cluster_weights[:, MinIndex] = cluster_weights[:, MinIndex]/np.linalg.norm(cluster_weights[:,MinIndex])         
             
                self.cluster_distances[random_ordered[Sample]] = MinMetric
                if (i_iteration == (n_iterations-1)) :
                    Tempcluster_indices[random_ordered[Sample]] = MinIndex
                    cluster_histogram[MinIndex] = cluster_histogram[MinIndex]+1

            # Since we're talking about distances from the cluster
            # center, which is in some ways an average of pixel positions,
            # we use (npixels-1) in the denominator.
            New_RMSDistanceIterations[i_iteration] = np.sqrt(np.sum(self.cluster_distances**2)/float(npixels-1))
      



        # Next we sort the data with the cluster with the most members first
        count_indices = np.argsort(cluster_histogram)
        count_indices = count_indices[::-1]
                 
        cluster_histogram = cluster_histogram[count_indices]
        self.cluster_indices = np.zeros((npixels), dtype=int)
        ClustersFound = 0
        for i_cluster in range(nclusters):
            i_temp_cluster = count_indices[i_cluster]
            these_pixels = np.where(Tempcluster_indices == i_temp_cluster)[0]
            if len(these_pixels) > 0:
                cluster_indices[these_pixels] = i_cluster
                ClustersFound = ClustersFound + 1


        # Next we sort the cluster_weights with the cluster with the most
        # members first
        temp_weights = cluster_weights.copy()
        for i_cluster in range(ClustersFound):
            cluster_weights[0:nsigpca, i_cluster] = temp_weights[0:nsigpca, count_indices[i_cluster]]
          
        
        
        cluster_histogram = cluster_histogram[0:ClustersFound]
        cluster_weights = cluster_weights[:, 0:ClustersFound]
        
        
#         # Recalculate the cluster centers to be equal to the average of the
#         # pixel weights. For angle measure will be done later.
#         for i_cen in range(nclusters):
#             cluster_members = np.where(cluster_indices == i_cen)
#             n_mem = len(cluster_members[0])
#             if len(cluster_members[0])> 0:
#                 WorkRow2=np.ones((n_mem))
#                 cluster_weights[:,i_cen]=np.dot(od_reduced[:,cluster_members],WorkRow2)/n_mem
        

        self.cluster_distances = np.reshape(self.cluster_distances, (self.stack.n_cols, self.stack.n_rows), order='F')
        self.cluster_indices = cluster_indices
        
        self.clustersizes = cluster_histogram
                          
        self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
        self.sse = np.zeros((npixels))
               
        for i in range(nclusters):
            clind = np.where(self.cluster_indices == count_indices[i])
            self.clustersizes[i] = self.cluster_indices[clind].shape[0]

            for ie in range(self.stack.n_ev):  
                thiseng_od = self.stack.od[:,ie]
                self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
                
      
        #Calculate SSE Sum of Squared errors          
        for i in range(npixels):
            clind = self.cluster_indices[i]
            self.sse[i] = np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:]))         
            
        self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        


        if (sigmasplit ==1):
            #Check the validity of cluster analysis and if needed add another cluster
            new_cluster_indices = self.cluster_indices.copy()
            new_nclusters = nclusters
            recalc_clusters = False
             
            for i in range(nclusters):
                clind = np.where(self.cluster_indices == i)
                cl_sse_mean = np.mean(self.sse[clind])
                cl_see_std = np.std(self.sse[clind])
                 
                sigma9 = cl_sse_mean+9*cl_see_std
                maxsse = np.max(self.sse[clind])
                if (maxsse > sigma9): 
                    recalc_clusters = True
                    sse_helper = np.zeros((self.stack.n_cols, self.stack.n_rows), dtype=int)
                    sse_helper[clind] = self.sse[clind]
                    newcluster_ind = np.where(sse_helper > sigma9)
                    new_cluster_indices[newcluster_ind] = new_nclusters
                    new_nclusters += 1
                            
            
            if recalc_clusters == True:
                nclusters = new_nclusters
                self.cluster_indices = new_cluster_indices
                self.clusterspectra = np.zeros((nclusters, self.stack.n_ev))
                self.clustersizes = np.zeros((nclusters,), dtype=int)
                for i in range(nclusters):
                    clind = np.where(self.cluster_indices == i)
                    self.clustersizes[i] = self.cluster_indices[clind].shape[0]
                    if self.clustersizes[i]>0:
                        for ie in range(self.stack.n_ev):  
                            thiseng_od = self.stack.od3d[:,:,ie]
                            self.clusterspectra[i,ie] = np.sum(thiseng_od[clind])/self.clustersizes[i]
              
                #Calculate SSE Sum of Squared errors
                indx = np.reshape(self.cluster_indices, (npixels), order='F')
                self.sse = np.zeros((npixels))
                for i in range(npixels):
                    clind = indx[i]
                    self.sse[i] = np.sqrt(np.sum(np.square(self.stack.od[i,:]-self.clusterspectra[clind,:])))         
                    
                self.sse = np.reshape(self.sse, (self.stack.n_cols, self.stack.n_rows), order='F')
        
        
        self.cluster_indices = np.reshape(self.cluster_indices, (self.stack.n_cols, self.stack.n_rows), order='F') 
        self.cluster_distances = self.sse
        
        self.clusters_calculated = 1
        
        return int(nclusters)
        
    


#-----------------------------------------------------------------------   
# Find clusters using EM clustering
    def calculate_clusters_em(self, nclusters):
        #Reduced data matrix od_reduced(n_pixels,n_significant_components)
        #od_reduced = np.zeros((self.stack.n_cols, self.stack.n_rows, self.numsigpca))
        

        npixels = self.stack.n_cols * self.stack.n_rows
        
        inverse_n_pixels = 1./float(npixels)
        
        od_reduced = self.pcaimages[:,:,0:self.numsigpca]
        
        od_reduced = np.reshape(od_reduced, (npixels,self.numsigpca), order='F')
        

        #kmeans(obs,k_or_guess,iter=20,thresh=1e-5)
        
        self.indx = np.zeros(npixels)

        res, self.indx = kmeans2(od_reduced,5)
        
        self.indx = np.reshape(self.indx, (self.stack.n_cols, self.stack.n_rows), order='F')  
        
        
#------------------------------------------------------------------------------
# Spectral analysis
# This routine reads in a mu spectrum in units of inverse microns.
# The spectrum is interpolated onto the energy range of the stack,
# and loaded into the matrix target_spectra(pca_gui_par.n_targets,n_ev).
# If there is a PCA calculation done, we find the fits to the
# target spectra from the components.
    def read_target_spectrum(self, filename = '', flat = False):
        # Load spectrum from a file
        #Todo: preparation completed. combine with load_xraypeakfit_spectra
        if flat == True:
            spectrum_evdata = self.stack.ev
            spec = np.ones((1,self.stack.n_ev))
            spectrum_common_names= ['Flat']
            fitspectra = self.remap_spectrum(spectrum_evdata, spec)
        else:
            spectrum_common_names, fitspectra = self.load_filter_remap_spectra(filename)
        self.n_target_to_add = len(spectrum_common_names)
        if self.n_target_to_add:
            self.n_target_spectra += self.n_target_to_add
            try:
                self.target_spectra = np.vstack((self.target_spectra,fitspectra))
            except ValueError:
                self.target_spectra = fitspectra
                self.tspectrum_loaded = 1
            for i, n in enumerate(spectrum_common_names):
                name = n.strip()
                if name == ' ':
                    name = 'Cluster %d' % (i+1)
                if name in self.tspec_names:
                    name = name + '_' + str(len(list(filter(lambda x: x.startswith(name), self.tspec_names))))
                self.tspec_names.append(name)

        self.fit_target_spectra()
        self.calc_svd_maps()
        

            
#------------------------------------------------------------------------------          
    def add_cluster_target_spectra(self):
        # Load spectrum from a file or cluster spectra 


        for i in range(self.nclusters):
   
            target_spectrum = self.clusterspectra[i,:]
       
            if self.tspectrum_loaded == 0:
                self.target_spectra = target_spectrum
                self.tspectrum_loaded = 1
                self.n_target_spectra += 1
            else:
                self.target_spectra = np.vstack((self.target_spectra,target_spectrum))
                self.n_target_spectra += 1
            self.tspec_names.append('Cluster '+str(i+1))

        self.fit_target_spectra()
        self.calc_svd_maps()  
        
#------------------------------------------------------------------------------          
    def remove_spectrum(self, i_spec):   
        
        if self.n_target_spectra > 1:
            self.target_spectra = np.delete(self.target_spectra, i_spec, axis=0)
            
            del self.tspec_names[i_spec]
            self.n_target_spectra -= 1
        
            self.fit_target_spectra()
            self.calc_svd_maps()  
        else:
            self.target_spectra = []
            self.tspec_names = []
            self.tspectrum_loaded = 0
            self.n_target_spectra = 0
            
            self.target_svd_maps4D = []
            self.original_svd_maps4D = []
            self.target_pcafit_maps4D = []
            self.original_fit_maps4D = []
            self.target_pcafit_coeffs4D = []
            self.target_pcafit_spectra4D = []
            
#------------------------------------------------------------------------------          
    def move_spectrum(self, old_position, new_position):   
        
        temp_target_spectra = self.target_spectra.copy()
        temp_target_spectra[old_position,:] = self.target_spectra[new_position,:]
        temp_target_spectra[new_position,:] = self.target_spectra[old_position,:]
        self.target_spectra = temp_target_spectra
        
        temp_tspec_name = self.tspec_names[new_position]
        self.tspec_names[new_position] = self.tspec_names[old_position]
        self.tspec_names[old_position] = temp_tspec_name
                
        self.fit_target_spectra()
        self.calc_svd_maps()  

        
#------------------------------------------------------------------------------
# This routine calculates:
#   - the transformation matrix T as target_spectrumfit_coeffs, and
#     the fits to the target spectra as target_fittedspectra
#   - the inverse of T
#   - the eigenvector matrix C(S_abstract,N) by transposing the matrix
#       CT(N,S_abstract)=evecs(N,S_abstract)
#   - the target maps t(P,S_targets) as targetfit_maps
    def fit_target_spectra(self):
#       We want to find T(S_physical,S_abstract) which is
#       CT(N,S_abstract)##mu(S_physical,N).  But mu(S_physical,N) is
#       known here as target_spectra(S_physical,N), and
#       CT(N,S_abstract) is just a limited version of evecs(N,S).
#       We will call T(S_physical,S_abstract) by the name
#       target_spectrumfit_coeffs(S_physical,S_abstract).


        if self.pca_calculated == 0:
            return
        
        CT = self.eigenvecs[:,0:self.numsigpca]
        self.target_pcafit_coeffs = np.dot(self.target_spectra, CT )
      
#       Now we get the target spectra as approximated from our
#       components with
#       mu(S_physical,N)=C(S_abstract,N)##T(S_physical,S_abstract).
        self.target_pcafit_spectra = np.dot(self.target_pcafit_coeffs, CT.T)

#       To get the maps, we need to find R(P,Sbar_abstract)
#       from ct(N,Sbar_abstract)##d(P,N), and we also
#       need to invert the transformation matrix.  Start by
#       finding the singular value decomposition of the
#       transformation matrix.
        U, s, V = np.linalg.svd(self.target_pcafit_coeffs, full_matrices=False)
                        
#       This gives T^{-1}(Sbar_abstract,S_physical)
        t_inverse = np.dot(np.dot(V.T, np.linalg.inv(np.diag(s))), U.T)
     
#       This is R(P,Sbar_abstract)=CT(N,Sbar_abstract)##D(P,N)
        r_matrix = np.dot(self.stack.od, CT)

#       and this gives us the maps as
#       t(P,S_physical) = T^{-1}(Sbar_abstract,S_physical)##R(P,Sbar_abstract)
#       but in fact it is t(P,n_targets)!
        self.target_pcafit_maps = np.dot(r_matrix, t_inverse)
        
        self.target_pcafit_maps = np.reshape(self.target_pcafit_maps, 
                                             (self.stack.n_cols, self.stack.n_rows, self.n_target_spectra), order='F')  
        
        
        self.original_fit_maps = self.target_pcafit_maps.copy()
        
        #Find fit errors
        self.target_rms = (self.target_spectra-self.target_pcafit_spectra)**2
        self.target_rms = np.sqrt(np.sum(self.target_rms, axis=1)/self.stack.n_ev)
        
        return

    
#------------------------------------------------------------------------------
# This routine calculates the SVD composition maps
#   1. The optical density is calculated from the stack, and the
#      data matrix D of dimensions (pixels,energies) is formed
#   2. mu_inverse is calculated using SVD
#   3. svd_maps is calculated from multiplying mu_inverse ## d
    def calc_svd_maps(self, usefittedspectra = False):
        
        if usefittedspectra:
            U, s, V = np.linalg.svd(self.target_pcafit_spectra, full_matrices=False)
        else:
            U, s, V = np.linalg.svd(self.target_spectra, full_matrices=False)
            
      
        mu_inverse = t_inverse = np.dot(np.dot(V.T, np.linalg.inv(np.diag(s))), U.T)
        self.target_svd_maps = np.dot(self.stack.od, mu_inverse)
        
        self.target_svd_maps = np.reshape(self.target_svd_maps, 
                                             (self.stack.n_cols, self.stack.n_rows, self.n_target_spectra), order='F') 
        

        self.original_svd_maps = self.target_svd_maps.copy()
        
        
        
#-----------------------------------------------------------------------   
# Calculate composition maps for 4D data 
    def calculate_targetmaps_4D(self):
        
        n_pix = self.stack.n_cols*self.stack.n_rows
        
        self.target_svd_maps4D = []
        self.original_svd_maps4D = []
        self.target_pcafit_maps4D = []
        self.original_fit_maps4D = []
        self.target_pcafit_coeffs4D = []
        self.target_pcafit_spectra4D = []
        
        tempod = self.stack.od.copy()
        
        for jth in range(self.stack.n_theta):
            
            
            od3d = self.stack.od4D[:,:,:,jth]
            od = od3d.copy()
            
            self.stack.od = np.reshape(od, (n_pix, self.stack.n_ev), order='F')   
            
            if len(self.eigenvecs4D) > 0:
                self.eigenvecs = self.eigenvecs4D[jth]
                self.fit_target_spectra()
                
            self.calc_svd_maps()  
            
            self.target_svd_maps4D.append(self.target_svd_maps)
            self.original_svd_maps4D.append(self.original_svd_maps)
            if len(self.eigenvecs4D) > 0:
                self.target_pcafit_maps4D.append(self.target_pcafit_maps)
                self.original_fit_maps4D.append(self.original_fit_maps)
                self.target_pcafit_coeffs4D.append(self.target_pcafit_coeffs)
                self.target_pcafit_spectra4D.append(self.target_pcafit_spectra)
            
        
        self.stack.od = tempod   
            
            
#------------------------------------------------------------------------------
# Apply threshold on SVD or PCA maps
    def svd_map_threshold(self, cutoff1, cutoff2 = None, svd = False, pca = False):
        
        if svd:
            self.target_svd_maps = self.original_svd_maps.copy()
            self.target_svd_maps.clip(min=cutoff1, out=self.target_svd_maps)
            if cutoff2 != None:
                self.target_svd_maps.clip(max=cutoff2, out=self.target_svd_maps)
                
            if len(self.target_svd_maps4D) > 0:
                self.target_svd_maps4D = copy.deepcopy(self.original_svd_maps4D)
                if cutoff2 != None:
                    maxclip = cutoff2
                else:
                    maxclip = np.amax(self.target_svd_maps4D)
                self.target_svd_maps4D = np.clip(self.target_svd_maps4D, cutoff1, maxclip)

                
        if pca:
            self.target_pcafit_maps = self.original_fit_maps.copy()
            self.target_pcafit_maps.clip(min=cutoff1, out=self.target_pcafit_maps)
            if cutoff2 != None:
                self.target_pcafit_maps.clip(max=cutoff2, out=self.target_pcafit_maps)
                
            if len(self.target_pcafit_maps) > 0:
                self.target_pcafit_maps4D = copy.deepcopy(self.original_fit_maps4D)
                if cutoff2 != None:
                    maxclip = cutoff2
                else:
                    maxclip = np.amax(self.target_pcafit_maps4D)                
                self.target_pcafit_maps4D = np.clip(self.target_pcafit_maps4D, cutoff1, maxclip)

              

#------------------------------------------------------------------------------
# Find key energies by finding peaks and valleys in significant pca spectra
    def calc_key_engs(self, threshold):
 
        key_engs = []
               
        for i in range(self.numsigpca):
            pcaspectrum = self.eigenvecs[:,i]
            pmax,pmin = self.find_peaks(pcaspectrum, threshold, x = self.stack.ev)
            
            for i in range(len(pmin)):
                key_engs.append(pmin[i][0])
            for i in range(len(pmax)):
                key_engs.append(pmax[i][0])
                
        key_engs = np.array(key_engs)
        
        #Sort the energies and remove double entries
        key_engs = np.unique(key_engs)
                
        return key_engs



#------------------------------------------------------------------------------
#Peakfinder
    def find_peaks(self, v, delta, x = None):
        """
        Converted from MATLAB script at http://billauer.co.il/peakdet.html by endolith
        https://gist.github.com/250860
        Returns two arrays
        function [maxtab, mintab]=peakdet(v, delta, x)
        %PEAKDET Detect peaks in a vector
        % [MAXTAB, MINTAB] = PEAKDET(V, DELTA) finds the local
        % maxima and minima ("peaks") in the vector V.
        % MAXTAB and MINTAB consists of two columns. Column 1
        % contains indices in V, and column 2 the found values.
        %
        % With [MAXTAB, MINTAB] = PEAKDET(V, DELTA, X) the indices
        % in MAXTAB and MINTAB are replaced with the corresponding
        % X-values.
        %
        % A point is considered a maximum peak if it has the maximal
        % value, and was preceded (to the left) by a value lower by
        % DELTA.
        % Eli Billauer, 3.4.05 (Explicitly not copyrighted).
        % This function is released to the public domain; Any use is allowed.
        """
        maxtab = []
        mintab = []
        
        if x is None:
            x = np.arange(len(v))
        v = np.asarray(v)

        if len(v) != len(x):
            print('Input vectors v and x must have same length')
            return -1
        if not np.isscalar(delta):
            print('Input argument delta must be a scalar')
            return -1
        if delta <= 0:
            print('Input argument delta must be positive')
            return -1
        mn, mx = np.inf, -np.inf
        mnpos, mxpos = np.nan, np.nan
        lookformax = True
        for i in np.arange(len(v)):
            this = v[i]
            if this > mx:
                mx = this
                mxpos = x[i]
            if this < mn:
                mn = this
                mnpos = x[i]
            if lookformax:
                if this < mx-delta:
                    maxtab.append((mxpos, mx))
                    mn = this
                    mnpos = x[i]
                    lookformax = False
            else:
                if this > mn+delta:
                    mintab.append((mnpos, mn))
                    mx = this
                    mxpos = x[i]
                    lookformax = True
        return np.array(maxtab), np.array(mintab)

# ----------------------------------------------------------------------
    def load_filter_remap_spectra(self, filename):
        spectrum_common_names = [' ']
        fitspectra = 0
        fn = os.path.basename(str(filename))
        _, extension = os.path.splitext(fn)
        if extension in ['.csv', '.txt', '.xas']:
            spectrum_evdata, spectrum_data, spectrum_common_names = file_stk.read_ascii(self, filename)
        for i, spec in enumerate(spectrum_data):
            if not np.any(spec):  # exclude spectra with only zeros
                spectrum_common_names.pop(i)
                continue
            fitspectrum = self.remap_spectrum(spectrum_evdata, spec)

            try:
                fitspectra = np.vstack((fitspectra, fitspectrum))
            except ValueError:
                fitspectra = fitspectrum

        return spectrum_common_names, fitspectra

# ----------------------------------------------------------------------
    def remap_spectrum(self, spec_ev, spec):
        if self.stack.n_ev > 0:
            # Map this spectrum onto our energy range - interpolate to ev
            ftspec = scipy.interpolate.interp1d(spec_ev, spec, kind='cubic', bounds_error=False)
            fit_spectrum = np.reshape(ftspec(self.stack.ev), (1, self.stack.n_ev))
        else:
            self.stack.ev = spec_ev
            self.stack.n_ev = len(self.stack.ev)
            fit_spectrum = np.reshape(spec, (1, self.stack.n_ev))

        # fix the edges if needed
        if self.stack.ev[0] < spec_ev[0]:
            indx = np.where(self.stack.ev < spec_ev[0])
            fit_spectrum[0, indx] = spec[0]
        if self.stack.ev[-1] > spec_ev[-1]:
            indx = np.where(self.stack.ev > spec_ev[-1])
            fit_spectrum[0, indx] = spec[-1]
        return fit_spectrum

#-----------------------------------------------------------------------   
    def load_xraypeakfit_spectra(self, filename, flat = False):
        # Load spectrum from a file
        #Todo: preparation completed. combine with read_target_spectrum
        if flat == True:
            spectrum_evdata = self.stack.ev
            spec = np.ones((1,self.stack.n_ev))
            spectrum_common_names= ['Flat']
            fitspectra = self.remap_spectrum(spectrum_evdata, spec)
        else:
            spectrum_common_names, fitspectra = self.load_filter_remap_spectra(filename)
        self.xrayfitsp_to_add = len(spectrum_common_names)
        if self.xrayfitsp_to_add:
            self.n_xrayfitsp += self.xrayfitsp_to_add
            try:
                self.xrayfitspectra = np.vstack((self.xrayfitspectra, fitspectra))
            except ValueError:
                self.xrayfitspectra = fitspectra
                self.xrayfitsp_loaded = 1
            for i, n in enumerate(spectrum_common_names):
                name = n.strip()
                if name == ' ':
                    name = 'Spectrum %d' % (i+1)
                if name in self.xfspec_names:
                    name = name +'_'+ str(len(list(filter(lambda x: x.startswith(name), self.xfspec_names))))
                self.xfspec_names.append(name)

                self.xfitpars.append(Cfitparams())
                #Find peaks:
                self.init_fit_params(self.n_xrayfitsp-self.xrayfitsp_to_add+i)

#-----------------------------------------------------------------------   
#Load spectra from cluster analysis
    def load_xraypeakfit_clusterspectrum(self, i_cluster):


        xfit_spectrum = self.clusterspectra[i_cluster,:].copy()
        xfit_spectrum = np.reshape(xfit_spectrum, (1,self.stack.n_ev)) 
        spectrum_common_name = 'Cluster '+str(i_cluster+1)

        if self.xrayfitsp_loaded == 0:
            self.xrayfitspectra = xfit_spectrum.copy()
            self.xrayfitsp_loaded = 1
            self.n_xrayfitsp = 1
        else:
            self.xrayfitspectra = np.vstack((self.xrayfitspectra, xfit_spectrum))
            self.n_xrayfitsp += 1
            
        self.xfspec_names.append(spectrum_common_name)
        
        self.xfitpars.append(Cfitparams())
        #Find peaks:
        self.init_fit_params(self.n_xrayfitsp-1)
        
#-----------------------------------------------------------------------   
    def init_fit_params(self, index):
        
        
        pmax,pmin = self.find_peaks(self.xrayfitspectra[index], 0.03, x = self.stack.ev)
        fp = self.xfitpars[index]

        peakengs = []
        if len(pmax) > 0:
            for i in range(12):
                if i < len(pmax):
                    peakengs.append(pmax[i][0])
                else:
                    peakengs.append(0)
        else:
            delta = int(self.stack.n_ev/13)
            for i in range(12):
                peakengs.append(self.stack.ev[delta*i])
        
        fp.stepfitparams = [peakengs[0], 0.5, 3.0, peakengs[1], 0.5, 3.0]

        
        for i in range(12):
            fp.gauss_fp_a[i] = 1.0
            fp.gauss_fp_m[i] = peakengs[i]
            fp.gauss_fp_s[i] = 0.5 
            
            
        fp.base = np.mean(self.xrayfitspectra[index][0:5])
        
        self.set_init_fit_params(index, fp.base, fp.stepfitparams, fp.gauss_fp_a, fp.gauss_fp_m, fp.gauss_fp_s)
        
        return fp.base, fp.stepfitparams, fp.gauss_fp_a, fp.gauss_fp_m, fp.gauss_fp_s


#-----------------------------------------------------------------------   
    def set_init_fit_params(self, index, base, stepfitparams, peak_a, peak_m, peak_s):
        
        
        self.xfitpars[index].base = base
        
        
        self.xfitpars[index].stepfitparams = stepfitparams

        
        for i in range(12):
            self.xfitpars[index].gauss_fp_a[i] = peak_a[i]
            self.xfitpars[index].gauss_fp_m[i] = peak_m[i]
            self.xfitpars[index].gauss_fp_s[i] = peak_s[i]
            
            
        return

#-----------------------------------------------------------------------   
    def fit_spectrum(self, i_spec, nsteps, npeaks):
        
        
        xfit_spectrum = self.xrayfitspectra[i_spec]
        fp = self.xfitpars[i_spec]
        
        
        p = []
        self.nsteps = nsteps
        self.npeaks = npeaks
        
        p.append(fp.base)
        
        for i in range(nsteps*3):
            p.append(fp.stepfitparams[i])
            
        for i in range(npeaks):
            p.append(fp.gauss_fp_a[i])
            p.append(fp.gauss_fp_m[i])
            p.append(fp.gauss_fp_s[i])
            
        
        #p2, success = optimize.leastsq(model_error, p[:], args=(nsteps, npeaks, np.array(self.stack.ev).astype(np.float64), np.array(xfit_spectrum).astype(np.float64)))
        
        bounds=[]
        #base can be a negative number
        bounds.append((fp.base-1.5,fp.base+1.5))
        for i in range(1,len(p)):
            bmin = 0
            bmax = None
            bounds.append((bmin,bmax))
          
                    
        p2, success = leastsqbound(model_error, p[:], bounds, args=(nsteps, npeaks, np.array(self.stack.ev).astype(np.float64), np.array(xfit_spectrum).astype(np.float64)))
       
        fp.stepfitparams = np.zeros((8))
        fp.gauss_fp_a = np.zeros((12))
        fp.gauss_fp_m = np.zeros((12))
        fp.gauss_fp_s = np.zeros((12)) 
        
        fp.base = p2[0]
        fp.stepfitparams[0:nsteps*3] = p2[1:nsteps*3+1]
        for i in range(npeaks):           
            fp.gauss_fp_a[i] = p2[nsteps*3+1+i*3]
            fp.gauss_fp_m[i] = p2[nsteps*3+2+i*3]
            fp.gauss_fp_s[i] = p2[nsteps*3+3+i*3]
        
        
        y = model(p2, nsteps, npeaks, self.stack.ev)
        
        separate_y = []
        
        #Add base
        y1 = np.ones((self.stack.ev.size))*fp.base
        separate_y.append(y1)
        
        #Add step
        for i in range(nsteps):
            y1 = np.zeros((self.stack.ev.size))
            istepfitparams = p2[3*i+1:3*i+4]
            for i in range(self.stack.ev.size):
                y1[i] = stepfunc(istepfitparams, self.stack.ev[i])
            separate_y.append(y1)
            
        #Add peaks
        pg = nsteps*3+1
        for i in range(npeaks):
            pp = [p2[pg+i*3],p2[pg+1+i*3],p2[pg+2+i*3]]
            y1 = gaussian(pp,self.stack.ev)
            separate_y.append(y1)
           
        return y, separate_y


#-----------------------------------------------------------------------   
# Calculate Fast Independent Component Analysis; FASTICA uses Hyvarinen's 
# fixed-point algorithm 
# A. Hyvarinen. Fast and Robust Fixed-Point Algorithms for Independent 
#   Component Analysis. IEEE Transactions on Neural Networks 10(3):626-634, 1999.

    def calculate_fastica(self, mixedsig, numOfIC):

        mixedsig = mixedsig.transpose()
        
        print('msig', mixedsig.shape)

        # Remove the mean and check the data
        mixedmean = np.mean(mixedsig, axis=1)
        mixedsig = mixedsig - mixedmean[:,np.newaxis]

        Dim = mixedsig.shape[0]
        NumOfSampl = mixedsig.shape[1]
        
        print('Dim, NumOfSampl',Dim,NumOfSampl)


        # Default values for optional parameters
        verbose  = True

        # Default values for 'pcamat' parameters
        firstEig          = 1
        lastEig           = Dim
        interactivePCA    = 'off'

        # Default values for 'fpica' parameters
        approach          = 'defl'
        g                 = 'pow3'
        finetune          = 'off'
        a1                = 1
        a2                = 1
        myy               = 1
        stabilization     = 'off'
        epsilon           = 0.0001
        maxNumIterations  = 1000
        maxFinetune       = 5
        initState         = 'rand'
        guess             = 0
        sampleSize        = 1
        displayMode       = 'off'
        displayInterval   = 1


        # Parameters for fastICA 
        b_verbose = True
        

        # print information about data
        if b_verbose:
            print('Number of signals:', Dim)
            print('Number of samples: ', NumOfSampl)


        # Check if the data has been entered the wrong way,
        # but warn only... it may be on purpose

        if (Dim > NumOfSampl):
            if b_verbose:
                print('Warning: ')
                print('The signal matrix may be oriented in the wrong way.')
                print('In that case transpose the matrix.')

    
        # Calculating PCA
        # We already have the PCA data

        if b_verbose:
            print('Values for PCA calculations supplied.\n')
            print('PCA calculations not needed.\n')
    
        # PCA was already calculated:
        D = np.identity(self.numsigpca)*self.eigenvals[0:self.numsigpca] 
        E = self.eigenvecs[:,0:self.numsigpca]


        # Calculate the whitening
           
        # Calculate the whitening and dewhitening matrices (these handle
        # dimensionality simultaneously).
        whiteningMatrix = np.dot(np.linalg.inv (np.sqrt(D)), E.transpose())
        dewhiteningMatrix = np.dot(E, np.sqrt(D))
        
        print('wd=', whiteningMatrix.shape, dewhiteningMatrix.shape)
        
        # Project to the eigenvectors of the covariance matrix.
        # Whiten the samples and reduce dimension simultaneously.
        if b_verbose:
            print('Whitening...')
            whitesig =  np.dot(whiteningMatrix,mixedsig)
            print('whitesig', whitesig.shape)
            
        # Just some security...
        if np.sum(np.imag(whitesig)) != 0:
            print('Whitened vectors have imaginary values.')
            

        # Calculating the ICA
  
        # Check some parameters
        # The dimension of the data may have been reduced during PCA calculations.
        # The original dimension is calculated from the data by default, and the
        # number of IC is by default set to equal that dimension.
  
        Dim = whitesig.shape[0]
  
        # The number of IC's must be less or equal to the dimension of data
        if numOfIC > Dim:
            numOfIC = Dim
            # Show warning only if verbose = 'on' and user supplied a value for 'numOfIC'
            if b_verbose:
                print( 'Warning: estimating only ,', numOfIC, '  independent components' )
                print( '(Cannot estimate more independent components than dimension of data)')

  
        # Calculate the ICA with fixed point algorithm.
        A, W = self.calc_fpica (whitesig,  whiteningMatrix, dewhiteningMatrix, approach, 
          numOfIC, g, finetune, a1, a2, myy, stabilization, epsilon, 
          maxNumIterations, maxFinetune, initState, guess, sampleSize, 
          displayMode, displayInterval, verbose)
        
        print('A,W', A.shape, W.shape)
        # Check for valid return
        if W.any():
            # Add the mean back in.
            if b_verbose:
                print('Adding the mean back to the data.')
    
            icasig = np.dot(W, mixedsig) + np.dot(np.dot(W, mixedmean), np.ones((1, NumOfSampl)))

   
        else:
            icasig = []
            

        return icasig

#-----------------------------------------------------------------------   
    def getSamples(self, max, percentage):
        Samples = (np.random.random((max,)) < percentage).nonzero()
        return Samples
        
#-----------------------------------------------------------------------   
# Fixed point ICA
# This function is adapted from Hyvarinen's fixed point algorithm Matlab version
# [A, W] = fpica(whitesig, whiteningMatrix, dewhiteningMatrix, approach,
#        numOfIC, g, finetune, a1, a2, mu, stabilization, epsilon, 
#        maxNumIterations, maxFinetune, initState, guess, sampleSize,
#        displayMode, displayInterval, verbose);
# 
# Perform independent component analysis using Hyvarinen's fixed point
# algorithm. Outputs an estimate of the mixing matrix A and its inverse W.
#
# whitesig                              :the whitened data as row vectors
# whiteningMatrix                       :whitening matrix
# dewhiteningMatrix                     :dewhitening matrix
# approach      [ 'symm' | 'defl' ]     :the approach used (deflation or symmetric)
# numOfIC       [ 0 - Dim of whitesig ] :number of independent components estimated
# g             [ 'pow3' | 'tanh' |     :the nonlinearity used
#                 'gaus' | 'skew' ]     
# finetune      [same as g + 'off']     :the nonlinearity used in finetuning.
# a1                                    :parameter for tuning 'tanh'
# a2                                    :parameter for tuning 'gaus'
# mu                                    :step size in stabilized algorithm
# stabilization [ 'on' | 'off' ]        :if mu < 1 then automatically on
# epsilon                               :stopping criterion
# maxNumIterations                      :maximum number of iterations 
# maxFinetune                           :maximum number of iterations for finetuning
# initState     [ 'rand' | 'guess' ]    :initial guess or random initial state. See below
# guess                                 :initial guess for A. Ignored if initState = 'rand'
# sampleSize    [ 0 - 1 ]               :percentage of the samples used in one iteration
# displayMode   [ 'signals' | 'basis' | :plot running estimate
#                 'filters' | 'off' ]
# displayInterval                       :number of iterations we take between plots
# verbose       [ 'on' | 'off' ]        :report progress in text format

    def calc_fpica(self, X, whiteningMatrix, dewhiteningMatrix, approach, 
            numOfIC, g, finetune, a1, a2, myy, stabilization, 
            epsilon, maxNumIterations, maxFinetune, initState, 
            guess, sampleSize, displayMode, displayInterval, 
            b_verbose):
        

        vectorSize = X.shape[0]
        numSamples = X.shape[1]
        
        # Checking the value for approach
        if approach == 'symm':
            approachMode = 1
        elif approach == 'defl':
            approachMode = 2    
        else:
            print('Illegal value for parameter approach:', approach)
            return
        if b_verbose:
            print('Used approach:', approach)
            
        #Checking the value for numOfIC
        if vectorSize < numOfIC:
            print('Must have numOfIC <= Dimension!')
            return
        
        # Checking the sampleSize
        if sampleSize > 1:
            sampleSize = 1
            if b_verbose:
                print('Warning: Setting sampleSize to 1.\n')
   
        elif sampleSize < 1:
            if (sampleSize * numSamples) < 1000:
                sampleSize = np.min(1000/numSamples, 1)
                if b_verbose:
                    print('Warning: Setting ampleSize to ',sampleSize,' samples=', np.floor(sampleSize * numSamples))

        print('sample size', sampleSize)
        if  b_verbose and (sampleSize < 1):
            print('Using about  ',sampleSize*100,' of the samples in random order in every step.')
       
        
        # Checking the value for nonlinearity.

        if g == 'pow3':
            gOrig = 10
        elif g =='tanh':
            gOrig = 20
        elif g == 'gauss':
            gOrig = 30
        elif g == 'skew':
            gOrig = 40
        else:
            print('Illegal value for parameter g: ', g)

        if sampleSize != 1:
            gOrig = gOrig + 2

        if myy != 1:
            gOrig = gOrig + 1


        if b_verbose:
            print('Used nonlinearity: ', g)


        finetuningEnabled = 1
        if finetune == 'pow3':
            gFine = 10 + 1
        elif finetune == 'tanh':
            gFine = 20 + 1
        elif finetune == 'gauss':
            gFine = 30 + 1
        elif finetune == 'skew':
            gFine = 40 + 1
        elif finetune == 'off':
            if myy != 1:
                gFine = gOrig
            else :
                gFine = gOrig + 1
  
            finetuningEnabled = 0
        else:
            print('Illegal value for parameter finetune :', finetune)
            return

        if b_verbose and finetuningEnabled:
            print('Finetuning enabled, nonlinearity: ', finetune)


        if stabilization == 'on':
            stabilizationEnabled = 1
        elif stabilization == 'off':
            if myy != 1:
                stabilizationEnabled = 1
            else:
                stabilizationEnabled = 0

        else:
            print('Illegal value for parameter stabilization: ', stabilization)


        if b_verbose and stabilizationEnabled:
            print('Using stabilized algorithm.')
       
        # Some other parameters
        myyOrig = myy
        # When we start fine-tuning we'll set myy = myyK * myy
        myyK = 0.01
        # How many times do we try for convergence until we give up.
        failureLimit = 5


        usedNlinearity = gOrig
        stroke = 0
        notFine = 1
        long = 0
        
        
        # Checking the value for initial state.

        if initState == 'rand':
            initialStateMode = 0;
        elif initState == 'guess':
            if guess.shape[0] != whiteningMatrix.shape[1]:
                initialStateMode = 0
                if b_verbose:
                    print('Warning: size of initial guess is incorrect. Using random initial guess.')
    
            else:
                initialStateMode = 1
                if guess.shape[0] < numOfIC:
                    if b_verbose:
                        print('Warning: initial guess only for first ',guess.shape[0],' components. Using random initial guess for others.')
    
                    guess[:, guess.shape[0] + 1:numOfIC] = np.random.uniform(-0.5,0.5,(vectorSize,numOfIC-guess.shape[0]))
                elif guess.shape[0]>numOfIC:
                    guess=guess[:,1:numOfIC]
                    print('Warning: Initial guess too large. The excess column are dropped.')
  
                if b_verbose:
                    print( 'Using initial guess.')

        else:
            print('Illegal value for parameter initState:', initState)
            return        
        
        
        # Checking the value for display mode.

        if (displayMode =='off') or (displayMode == 'none'):
            usedDisplay = 0
        elif (displayMode =='on') or (displayMode == 'signals'):
            usedDisplay = 1
            if (b_verbose and (numSamples > 10000)):
                print('Warning: Data vectors are very long. Plotting may take long time.')
 
            if (b_verbose and (numOfIC > 25)):
                print('Warning: There are too many signals to plot. Plot may not look good.')
 
        elif (displayMode =='basis'):
            usedDisplay = 2
            if (b_verbose and (numOfIC > 25)):
                print( 'Warning: There are too many signals to plot. Plot may not look good.')
 
        elif (displayMode =='filters'):
            usedDisplay = 3
            if (b_verbose and (vectorSize > 25)):
                print('Warning: There are too many signals to plot. Plot may not look good.')

        else:
            print( 'Illegal value for parameter displayMode:', displayMode)
            return


        # The displayInterval can't be less than 1...
        if displayInterval < 1:
            displayInterval = 1

        # Start ICA calculation
        if b_verbose:
            print('Starting ICA calculation...')
            
            
        # SYMMETRIC APPROACH
        if approachMode == 1:
            print('Symmetric approach under construction')
            return
        
        
        # DEFLATION APPROACH
        elif approachMode == 2:      
            
            
            B = np.zeros((numOfIC, numOfIC))
  
            # The search for a basis vector is repeated numOfIC times.
            round = 0  
            numFailures = 0 
                      
            while (round < numOfIC):   
                myy = myyOrig
                usedNlinearity = gOrig
                stroke = 0
                notFine = 1
                long = 0
                endFinetuning = 0
                
                
                # Show the progress...
                if b_verbose:
                    print('IC :', round)
        
                # Take a random initial vector of length 1 and orthogonalize it
                # with respect to the other vectors.
                if initialStateMode == 0:
                    w = np.random.standard_normal((vectorSize,))
                    
                elif initialStateMode == 1:
                    w=np.dot(whiteningMatrix,guess[:,round])
                    
       
                w = w - np.dot(np.dot(B, B.T), w)
                norm =  np.sqrt((w*w).sum())
                w = w / norm
                
        
                wOld = np.zeros(w.shape)
                wOld2 = np.zeros(w.shape)
                    
                    
                # This is the actual fixed-point iteration loop.
                #    for i = 1 : maxNumIterations + 1
                i = 1
                gabba = 1
                while (i <= (maxNumIterations + gabba)):
                    if (usedDisplay > 0):
                        print('display')
          
                    #Project the vector into the space orthogonal to the space
                    # spanned by the earlier found basis vectors. Note that we can do
                    # the projection with matrix B, since the zero entries do not
                    # contribute to the projection.
                    w =  w - np.dot(np.dot(B, B.T), w)
                    norm =  np.sqrt((w*w).sum())
                    w = w / norm
    
                    if notFine:
                        if i == (maxNumIterations + 1):   
                            if b_verbose:
                                print('Component number',round,'  did not converge in ',maxNumIterations, 'iterations.')
          
                            round = round - 1
                            numFailures = numFailures + 1
                            if numFailures > failureLimit:
                                if b_verbose:
                                    print('Too many failures to converge ', numFailures,' Giving up.')
            
                                if round == 0:
                                    A=[]
                                    W=[]
                                    return
                            break                    
                    else:
                        if i >= endFinetuning:
                            #So the algorithm will stop on the next test...
                            wOld = w.copy()
                            
                    # Show the progress...
                    if b_verbose:
                        print( '.')
                        
                    # Test for termination condition. Note that the algorithm has
                    # converged if the direction of w and wOld is the same, this
                    # is why we test the two cases.
                    normm = np.sqrt(((w - wOld)*(w - wOld)).sum())
                    normp = np.sqrt(((w + wOld)*(w + wOld)).sum())
                    conv = min((normm), normp)
                    if  (conv < epsilon):
                        if finetuningEnabled and notFine:
                            if b_verbose:
                                print('Initial convergence, fine-tuning: ')
                            notFine = 0
                            gabba = maxFinetune
                            wOld = np.zeros(w.shape)
                            wOld2 = np.zeros(w.shape)
                            usedNlinearity = gFine
                            myy = myyK * myyOrig
          
                            endFinetuning = maxFinetune + i
                       
                        else:
                            numFailures = 0
                            # Save the vector
                            B[:, round] = w.copy()

          
                            # Calculate the de-whitened vector.
                            A = np.dot(dewhiteningMatrix, w)
                            # Calculate ICA filter.
                            W = np.dot(w.transpose(), whiteningMatrix)
                            
                            # Show the progress...
                            if b_verbose:
                                print('computed ( ',i,' steps ) ')
                                
                            break
                    elif stabilizationEnabled:
                                
                        if (not stroke) and (np.linalg.norm(w - wOld2) < epsilon or np.linalg.norm(w + wOld2) < epsilon):
                            stroke = myy
                            if b_verbose:
                                print('Stroke!')
                            myy = .5*myy
                            if np.mod(usedNlinearity,2) == 0:
                                usedNlinearity = usedNlinearity + 1
        
                        elif stroke:
                            myy = stroke
                            stroke = 0
                            if (myy == 1) and (np.mod(usedNlinearity,2) != 0):
                                usedNlinearity = usedNlinearity - 1
         
                        elif (notFine) and (not long) and (i > maxNumIterations / 2):
                            if b_verbose:
                                print( 'Taking long (reducing step size) ')
                                long = 1
                                myy = .5*myy
                                if np.mod(usedNlinearity,2) == 0:
                                    usedNlinearity = usedNlinearity + 1
    
          
                    wOld2 = wOld
                    wOld = w      
                                
                    # pow3
                    if usedNlinearity == 10:
                        u = np.dot(X.T, w)
                        w = np.dot(X, u*u*u)/numSamples - 3.*w

                    elif usedNlinearity == 11:
                        u = np.dot(X.T, w)
                        EXGpow3 = np.dot(X, u*u*u)/numSamples
                        Beta = np.dot(w.T, EXGpow3)
                        w = w - myy * (EXGpow3 - Beta*w)/(3-Beta)         
                                       
                    elif usedNlinearity == 12:
                        Xsub = self._get_rsamples(X)
                        u = np.dot(Xsub.T, w)
                        w = np.dot(Xsub, u*u*u)/Xsub.shape[1] - 3.*w

                    elif usedNlinearity == 13:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]                        
                        u = np.dot(Xsub.T, w)
                        EXGpow3 = np.dot(Xsub, u*u*u)/Xsub.shape[1]
                        Beta = np.dot(w.T, EXGpow3)
                        w = w - myy * (EXGpow3 - Beta*w)/(3-Beta)
                        
                    # tanh
                    elif usedNlinearity == 20:
                        u = np.dot(X.T, w)
                        tang = np.tanh(a1 * u)
                        temp = np.dot((1. - tang*tang).sum(axis=0), w)
                        w = (np.dot(X, tang) - a1*temp)/numSamples
                        
                    elif usedNlinearity == 21:
                        u = np.dot(X.T, w)
                        tang = np.tanh(a1 * u)
                        Beta = np.dot(u.T, tang)
                        temp = (1. - tang*tang).sum(axis=0)
                        w = w-myy*((np.dot(X, tang)-Beta*w)/(a1*temp-Beta))
                                                
                    elif usedNlinearity == 22:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        tang = np.tanh(a1 * u)
                        temp = np.dot((1. - tang*tang).sum(axis=0), w)
                        w = (np.dot(Xsub, tang) - a1*temp)/Xsub.shape[1]
                                                
                    elif usedNlinearity == 23:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        tang = np.tanh(a1 * u)
                        Beta = np.dot(u.T, tang)
                        w = w - myy * ((np.dot(Xsub, tang)-Beta*w) /
                                      (a1*(1. - tang*tang).sum(axis=0) -
                                       Beta))                        

                    # gauss
                    elif usedNlinearity == 30:
                        # This has been split for performance reasons.
                        u = np.dot(X.T, w)
                        u2 = u*u
                        ex = np.exp(-a2*u2*0.5)
                        gauss =  u*ex
                        dgauss = (1. - a2 *u2)*ex
                        w = (np.dot(X, gauss)-np.dot(dgauss.sum(axis=0), w))/numSamples
                        

                    elif usedNlinearity == 31:
                        u = np.dot(X.T, w)
                        u2 = u*u
                        ex = np.exp(-a2*u2*0.5)
                        gauss =  u*ex
                        dgauss = (1. - a2 *u2)*ex
                        Beta = np.dot(u.T, gauss)
                        w = w - myy*((np.dot(X, gauss)-Beta*w)/
                                    (dgauss.sum(axis=0)-Beta))                        
                        
                    elif usedNlinearity == 32:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        u2 = u*u
                        ex = np.exp(-a2*u2*0.5)
                        gauss =  u*ex
                        dgauss = (1. - a2 *u2)*ex
                        w = (np.dot(Xsub, gauss)-
                             np.dot(dgauss.sum(axis=0), w))/Xsub.shape[1]
                        
                    elif usedNlinearity == 33:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        u2 = u*u
                        ex = np.exp(-a2*u2*0.5)
                        gauss =  u*ex
                        dgauss = (1. - a2 *u2)*ex
                        Beta = np.dot(u.T, gauss)
                        w = w - myy*((np.dot(Xsub, gauss)-Beta*w)/
                                    (dgauss.sum(axis=0)-Beta))                        
                        
                    # skew
                    elif usedNlinearity == 40:
                        u = np.dot(X.T, w)
                        w = np.dot(X, u*u)/numSamples
                        
                    elif usedNlinearity == 41:
                        u = np.dot(X.T, w)
                        EXGskew = np.dot(X, u*u) / numSamples
                        Beta = np.dot(w.T, EXGskew)
                        w = w - myy * (EXGskew - np.dot(Beta, w))/(-Beta)
                        
                    elif usedNlinearity == 42:                 
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        w = np.dot(Xsub, u*u)/Xsub.shape[1]

                    elif usedNlinearity == 43:
                        Xsub=X[:,self.getSamples(numSamples, sampleSize)]
                        u = np.dot(Xsub.T, w)
                        EXGskew = np.dot(Xsub, u*u) / Xsub.shape[1]
                        Beta = np.dot(w.T, EXGskew)
                        w = w - myy * (EXGskew - Beta*w)/(-Beta)
                        
                    else:
                        print('Code for desired nonlinearity not found!')
                        return
                                 
                    # Normalize the new w.
                    norm = np.sqrt((w*w).sum())
                    w = w / norm
                    i = i + 1  
                round = round + 1
            
            if b_verbose:
                print('Done.')
            

        # In the end let's check the data for some security
        if A.imag.any():
            if b_verbose:
                print('Warning: removing the imaginary part from the result.')
            A = A.real
            W = W.imag

        return A, W
                
              
              
"""
Constrained multivariate Levenberg-Marquardt optimization
"""

from scipy.optimize import leastsq

def internal2external_grad(xi,bounds):
    """ 
    Calculate the internal to external gradiant
    
    Calculates the partial of external over internal
    
    """
    
    ge = np.empty_like(xi)

    for i,(v,bound) in enumerate(zip(xi,bounds)):
        
        a = bound[0]    # minimum
        b = bound[1]    # maximum

        if a == None and b == None:    # No constraints
            ge[i] = 1.0

        elif b == None:      # only min
            ge[i] = v/np.sqrt(v**2+1)

        elif a == None:      # only max
            ge[i] = -v/np.sqrt(v**2+1)

        else:       # both min and max
            ge[i] = (b-a)*np.cos(v)/2.

    return ge

def i2e_cov_x(xi,bounds,cov_x):

    grad = internal2external_grad(xi,bounds)
    grad =  np.atleast_2d(grad)
    return np.dot(grad.T,grad)*cov_x


def internal2external(xi,bounds):
    """ Convert a series of internal variables to external variables"""
    
    xe = np.empty_like(xi)

    for i,(v,bound) in enumerate(zip(xi,bounds)):
        
        a = bound[0]    # minimum
        b = bound[1]    # maximum

        if a == None and b == None:    # No constraints
            xe[i] = v

        elif b == None:      # only min
            xe[i] = a-1.+np.sqrt(v**2.+1.)

        elif a == None:      # only max
            xe[i] = b+1.-np.sqrt(v**2.+1.)

        else:       # both min and max
            xe[i] = a+((b-a)/2.)*( np.sin(v)+1.)

    return xe

def external2internal(xe,bounds):
    """ Convert a series of external variables to internal variables"""

    xi = np.empty_like(xe)

    for i,(v,bound) in enumerate(zip(xe,bounds)):
        
        a = bound[0]    # minimum
        b = bound[1]    # maximum

        if a == None and b == None: # No constraints
            xi[i] = v

        elif b == None:     # only min
            xi[i] = np.sqrt( (v-a+1.)**2.-1 )

        elif a == None:     # only max
            xi[i] = np.sqrt( (b-v+1.)**2.-1 )

        else:   # both min and max
            xi[i] = np.arcsin( (2.*(v-a)/(b-a))-1.)

    return xi

def err(p,bounds,efunc,args):
    
    pe = internal2external(p,bounds)    # convert to external variables
    return efunc(pe,*args)

def calc_cov_x(infodic,p):
    """
    Calculate cov_x from fjac, ipvt and p as is done in leastsq
    """

    fjac = infodic['fjac']
    ipvt = infodic['ipvt']
    n = len(p)

    # adapted from leastsq function in scipy/optimize/minpack.py
    perm = np.take(np.eye(n),ipvt-1,0)
    r = np.triu(np.transpose(fjac)[:n,:])
    R = np.dot(r,perm)
    #try:
    cov_x = np.linalg.inv(np.dot(np.transpose(R),R))
    #except LinAlgError:
    #    cov_x = None
    return cov_x


def leastsqbound(func,x0,bounds,args=(),**kw):
    """
    Constrained multivariant Levenberg-Marquard optimization

    Minimize the sum of squares of a given function using the 
    Levenberg-Marquard algorithm. Contraints on parameters are inforced using 
    variable transformations as described in the MINUIT User's Guide by
    Fred James and Matthias Winkler.

    Parameters:

    * func      functions to call for optimization.
    * x0        Starting estimate for the minimization.
    * bounds    (min,max) pair for each element of x, defining the bounds on
                that parameter.  Use None for one of min or max when there is
                no bound in that direction.
    * args      Any extra arguments to func are places in this tuple.

    Returns: (x,{cov_x,infodict,mesg},ier)

    Return is described in the scipy.optimize.leastsq function.  x and con_v  
    are corrected to take into account the parameter transformation, infodic 
    is not corrected.

    Additional keyword arguments are passed directly to the 
    scipy.optimize.leastsq algorithm. 

    """
    # check for full output
    if "full_output" in kw and kw["full_output"]:
        full=True
    else:
        full=False

    # convert x0 to internal variables
    i0 = external2internal(x0,bounds)

    # perfrom unconstrained optimization using internal variables
    r = leastsq(err,i0,args=(bounds,func,args),**kw)

    # unpack return convert to external variables and return
    if full:
        xi,cov_xi,infodic,mesg,ier = r
        xe = internal2external(xi,bounds)
        cov_xe = i2e_cov_x(xi,bounds,cov_xi)
        # XXX correct infodic 'fjac','ipvt', and 'qtf' 
        return xe,cov_xe,infodic,mesg,ier 

    else:
        xi,ier = r
        xe = internal2external(xi,bounds)
        return xe,ier
            
