├── LICENSE ├── README.md ├── data ├── FGM_debug.mat ├── H.mat └── trspec.mat ├── image ├── NMF_result.png ├── trace_spectra.png └── trspec.png ├── snpa.py ├── spectral_image_NMF.py └── time_resolved_spectra_NMF.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 lwchen6309 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Near separable NMF on time resolved spectra by Successive Nonnegative Projection Algorithm 2 | 3 | An python implementaion of 4 | 5 | **"Successive Nonnegative Projection Algorithm for Robust Nonnegative Blind Source Separation" 6 | by Gillis. (2014), doi : 10.1137/130946782 7 | 8 | **"Using Separable Nonnegative Matrix Factorization Techniques for the Analysis of Time-Resolved Raman Spectra" 9 | by Luce et al. (2016), doi : 10.1177/0003702816662600** 10 | 11 | This code is revised from the matlab code provided by the author. 12 | 13 | (matlab code : https://sites.google.com/site/nicolasgillis/code) 14 | 15 | # Introduction 16 | ## (1) Time-resolved sepctrum 17 | For fundamental knowledge of time-resolved spectrum, refer : 18 | https://en.wikipedia.org/wiki/Time-resolved_spectroscopy 19 | 20 | Given a time-resolved sepctrum M ∈ Rm×n+, (m : time slices, n : pixels) 21 | we aimed to factorized M = W * H, where 22 | W ∈ Rm×r+ is the temporal profiles of the species involved in the reaction, (kinetics) 23 | and H ∈ Rr×n+ is their hyperspectra. (ex : absorption spectra, emission spectrum, fluorescence spectra, etc.) 24 | The decomposition can be done by the nonnegative matrix factorization techniques. 25 | 26 | Since the general NMF problem is a difficult problem (ill-posed, NP hard), 27 | it is more practical to solve the "separable NMF" problem, which gives an approximated solution. 28 | **For details of NMF and separable-NMF, refer the Luce's work linked above.** 29 | 30 | 31 | ## (2) Near-separable matrix 32 | A matrix M is r-separable (near-separable) if there exist an index set K of cardinality r and a nonnegative matrix H ∈ Rr×n+ 33 | with M = M(:,K) H. That is, M can be spanned by a convex hull formed by M(:,K).   34 | 35 | **If you are confused by the idea of convex hull, imaging a r-dimensional space D ∈ Rr, where each axes correspond to M(:,K). 36 | The nonnegative H indicates that all elements of M has to live in the subspace D ∈ Rr+, which results a convex hull.** 37 | ex : for r = 3, the covex hull is the first quadrant. 38 | 39 | A time-resolved sepctrum M is near-separable, if 40 | **there exist a certain pixel that has only one hyperspectrum for all time slices.**   41 | Hence, the approximation of separable NMF highly depends on the overlapping between hyperspectra. 42 | **i.e. The less hyperspectra overlap, the better approximation you get.** 43 | 44 | 45 | # Run program 46 | (1) Install libraries : numpy, scipy, matplotlib 47 | 48 | (2) Run time_resolved_spectra_NMF.py 49 | 50 | # Results 51 | 52 | An artificial time-resolved spectra is given M = W * H: 53 | where 54 | 55 | ### W and H (time-resolved spectra) 56 | ![image](https://github.com/lwchen6309/successive-nonnegative-projection-algorithm./blob/master/image/trace_spectra.png) 57 | 58 | ### M (time-resolved spectra) 59 | ![image](https://github.com/lwchen6309/successive-nonnegative-projection-algorithm./blob/master/image/trspec.png) 60 | 61 | ## The result of SNPA 62 | from top to bottom: 63 | 64 | (1) H (hyperspectra from SNPA). 65 | 66 | (2) spectra at different time slices. 67 | 68 | (3) W(kinetics) and comparison of real ideal(real)-trace. 69 | 70 | 71 | ![image](https://github.com/lwchen6309/successive-nonnegative-projection-algorithm./blob/master/image/NMF_result.png) 72 | 73 | -------------------------------------------------------------------------------- /data/FGM_debug.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/data/FGM_debug.mat -------------------------------------------------------------------------------- /data/H.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/data/H.mat -------------------------------------------------------------------------------- /data/trspec.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/data/trspec.mat -------------------------------------------------------------------------------- /image/NMF_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/image/NMF_result.png -------------------------------------------------------------------------------- /image/trace_spectra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/image/trace_spectra.png -------------------------------------------------------------------------------- /image/trspec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwchen6309/successive-nonnegative-projection-algorithm/1faf37fb144b1e40cfb9da165ea54082b1b209a1/image/trspec.png -------------------------------------------------------------------------------- /snpa.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | """ 4 | A python implementation of successive nonnegative projection algorithm. 5 | 6 | Reference: 7 | "Successive Nonnegative Projection Algorithm for Robust Nonnegative Blind Source Separation" 8 | by Gillis. (2014), doi : 10.1137/130946782 9 | 10 | """ 11 | 12 | def simplexProj(y): 13 | """ 14 | Given y, computes its projection x* onto the simplex 15 | 16 | Delta = { x | x >= 0 and sum(x) <= 1 }, 17 | 18 | that is, x* = argmin_x ||x-y||_2 such that x in Delta. 19 | 20 | 21 | See Appendix A.1 in N. Gillis, Successive Nonnegative Projection 22 | Algorithm for Robust Nonnegative Blind Source Separation, arXiv, 2013. 23 | 24 | 25 | x = SimplexProj(y) 26 | 27 | ****** Input ****** 28 | y : input vector. 29 | 30 | ****** Output ****** 31 | x : projection of y onto Delta. 32 | """ 33 | 34 | if len(y.shape) == 1: #Reshape to (1,-1) if y is a vector. 35 | y = y.reshape(1,-1) 36 | 37 | x = y.copy() 38 | x[x < 0] = 0 39 | K = np.flatnonzero(np.sum(x,0) > 1) 40 | x[:,K] = blockSimplexProj(y[:,K]) 41 | return x 42 | 43 | def blockSimplexProj(y): 44 | """ Same as function SimplexProj except that sum(max(Y,0)) > 1. """ 45 | 46 | r, m = y.shape 47 | ys = -np.sort(-y,axis=0) 48 | mu = np.zeros(m, dtype=float) 49 | S = np.zeros((r,m), dtype=float) 50 | 51 | for i in range(1,r): 52 | S[i,:] = np.sum(ys[:i,:] - ys[i,:],0) 53 | colInd_ge1 = np.flatnonzero(S[i,:] >= 1) 54 | colInd_lt1 = np.flatnonzero(S[i,:] < 1) 55 | if len(colInd_ge1) > 0: 56 | mu[colInd_ge1] = (1-S[i-1,colInd_ge1]) / i - ys[i-1,colInd_ge1] 57 | if i == r: 58 | mu[colInd_lt1] = (1-S[r,colInd_lt1]) / (r + 1) - ys[r,colInd_lt1] 59 | x = y + mu 60 | x[x < 0] = 0 61 | return x 62 | 63 | def fastGrad_simplexProj(M, U, V=None, maxiter=500): 64 | """ 65 | Fast gradient method to solve least squares on the unit simplex. 66 | See Nesterov, Introductory Lectures on Convex Optimization: A Basic 67 | Course, Kluwer Academic Publisher, 2004. 68 | 69 | This code solves: 70 | 71 | min_{V(:,j) in Delta, forall j} ||M-UV||_F^2, 72 | 73 | where Delta = { x | sum x_i <= 1, x_i >= 0 for all i }. 74 | 75 | See also Appendix A in N. Gillis, Successive Nonnegative Projection 76 | Algorithm for Robust Nonnegative Blind Source Separation, arXiv, 2013. 77 | 78 | 79 | [V,e] = FGMfcnls(M,U,V,maxiter) 80 | 81 | ****** Input ****** 82 | M : m-by-n data matrix 83 | U : m-by-r basis matrix 84 | V : initialization for the fast gradient method 85 | (optional, use [] if none) 86 | maxiter: maximum numbre of iterations (default = 500). 87 | 88 | ****** Output ****** 89 | V : V(:,j) = argmin_{x in Delta} ||M-Ux||_F^2 forall j. 90 | e : e(i) = error at the ith iteration 91 | """ 92 | 93 | m, n = M.shape 94 | m, r = U.shape 95 | 96 | # Initialization of V 97 | if V is None: 98 | V = np.zeros((r,n),dtype=float) 99 | for col_M in range(n): 100 | # Distance between ith column of M and columns of U 101 | disti = np.sum((U - M[:,col_M].reshape(-1,1))**2, 0) 102 | min_col_U = np.argmin(disti) 103 | V[min_col_U, col_M] = 1 104 | 105 | # Hessian and Lipschitz constant 106 | UtU = U.T.dot(U) 107 | L = np.linalg.norm(UtU,ord=2) # 2-norm 108 | # Linear term 109 | UtM = U.T.dot(M) 110 | nM = np.linalg.norm(M)**2 # Frobenius norm 111 | # Projection 112 | alpha = [0.05, 0] # Parameter, can be tuned. 113 | err = [0, 0] 114 | V = simplexProj(V) # Project initialization onto the simplex 115 | Y = V # second sequence 116 | 117 | delta = 1e-6 118 | # Stop if ||V^{k}-V^{k+1}||_F <= delta * ||V^{0}-V^{1}||_F 119 | for i in range(maxiter): 120 | # Previous iterate 121 | Vprev = V 122 | # FGM Coefficients 123 | alpha[1] = (np.sqrt(alpha[0]**4 + 4*alpha[0]**2) - alpha[0]**2) / 2 124 | beta = alpha[0] * (1 - alpha[0]) / (alpha[0]**2 + alpha[1]) 125 | # Projected gradient step from Y 126 | V = simplexProj(Y - (UtU.dot(Y) - UtM) / L) 127 | # `Optimal' linear combination of iterates 128 | Y = V + beta * (V - Vprev) 129 | # Error 130 | err[1] = nM - 2 * np.sum(np.ravel(V*UtM)) + np.sum(np.ravel(UtU * V.dot(V.T))) 131 | 132 | # Restart: fast gradient methods do not guarantee the objective 133 | # function to decrease, a good heursitic seems to restart whenever it 134 | # increases although the global convergence rate is lost! This could 135 | # be commented out. 136 | if i > 0 and err[1] > err[0]: 137 | Y = V 138 | if i is 0: 139 | eps0 = np.linalg.norm(V - Vprev) 140 | eps = np.linalg.norm(V - Vprev) 141 | if eps < delta * eps0: 142 | break 143 | # Update 144 | alpha[0] = alpha[1] 145 | err[0] = err[1] 146 | 147 | return V, err[1] 148 | 149 | def snpa(M, r, normalize=False, maxitn=100): 150 | """ 151 | Successive Nonnegative Projection Algorithm (variant with f(.) = ||.||^2) 152 | 153 | *** Description *** 154 | At each step of the algorithm, the column of M maximizing ||.||_2 is 155 | extracted, and M is updated with the residual of the projection of its 156 | columns onto the convex hull of the columns extracted so far. 157 | 158 | See N. Gillis, Successive Nonnegative Projection Algorithm for Robust 159 | Nonnegative Blind Source Separation, arXiv, 2013. 160 | 161 | 162 | [J,H] = SNPA(M,r,normalize) 163 | 164 | ****** Input ****** 165 | M = WH + N : a (normalized) noisy separable matrix, that is, W is full rank, 166 | H = [I,H']P where I is the identity matrix, H'>= 0 and its 167 | columns sum to at most one, P is a permutation matrix, and 168 | N is sufficiently small. 169 | r : number of columns to be extracted. 170 | normalize : normalize=1 will scale the columns of M so that they sum to one, 171 | hence matrix H will satisfy the assumption above for any 172 | nonnegative separable matrix M. 173 | normalize=0 is the default value for which no scaling is 174 | performed. For example, in hyperspectral imaging, this 175 | assumption is already satisfied and normalization is not 176 | necessary. 177 | 178 | ****** Output ****** 179 | J : index set of the extracted columns. 180 | H : optimal weights, that is, H argmin_{X >= 0} ||M-M(:,K)X||_F 181 | """ 182 | 183 | m, n = M.shape 184 | 185 | if normalize: 186 | # Normalization of the columns of M so that they sum to one 187 | M /= (np.sum(M, 0) + 1e-15) 188 | 189 | normM = np.sum(M**2, 0) 190 | nM = np.max(normM) 191 | J = np.array([],dtype=int) 192 | # Perform r recursion steps (unless the relative approximation error is 193 | # smaller than 10^-9) 194 | for i in range(r): 195 | if np.max(normM) / nM <= 1e-9: 196 | break 197 | 198 | # Select the column of M with largest l2-norm 199 | b = np.argmax(normM) 200 | a = normM[b] 201 | 202 | # Norms of the columns of the input matrix M 203 | if i is 0: 204 | normM1 = normM.copy() 205 | 206 | # Check ties up to 1e-6 precision 207 | b = np.flatnonzero((a - normM) / a <= 1e-6) 208 | 209 | # In case of a tie, select column with largest norm of the input matrix M 210 | if len(b) > 1: 211 | d = np.argmax(normM1[b]) 212 | b = b[d] 213 | # Update the index set, and extracted column 214 | J = np.append(J,int(b)) 215 | 216 | # Update residual 217 | if i is 0: 218 | # Initialization using 10 iterations of coordinate descent 219 | # H = nnlsHALSupdt(M,M(:,J),[],10); 220 | # Fast gradient method for min_{y in Delta} ||M(:,i)-M(:,J)y|| 221 | H, _ = fastGrad_simplexProj(M,M[:,J],None,maxitn) 222 | else: 223 | H[:,J[i]] = 0 224 | h = np.zeros((1,n),dtype=float) 225 | h[0,J[i]] = 1 226 | H = np.vstack([H,h]) 227 | H, _ = fastGrad_simplexProj(M,M[:,J],H,maxitn) 228 | 229 | # Update norms 230 | R = M - M[:,J].dot(H) 231 | normM = np.sum(R**2, 0) 232 | 233 | return J, H 234 | 235 | import unittest 236 | class TestSimplexProj(unittest.TestCase): 237 | def test_blockSimplexProj(self): 238 | y = np.arange(-3,5).reshape(2,-1) 239 | x = blockSimplexProj(y) 240 | x_answer = np.array([[0, 0, 0, 0], 241 | [1, 1, 1, 1]]) 242 | np.testing.assert_array_equal(x, x_answer) 243 | 244 | def test_simplexProj(self): 245 | y = np.arange(-3,5).reshape(2,-1) 246 | x = simplexProj(y) 247 | x_answer = np.array([[0, 0, 0, 0], [1, 1, 1, 1]]) 248 | np.testing.assert_array_equal(x, x_answer) 249 | 250 | def test_fastGrad_simplexProj(self): 251 | # M = np.array([ 252 | # [8, 1, 6], 253 | # [3, 5, 7], 254 | # [4, 9, 2]]) / 10 255 | # x = np.ones(3,dtype=float).reshape(-1,1)/3 256 | # y = M.dot(x) 257 | # simplex_x, err = fastGrad_simplexProj(M,y) 258 | # simplex_x_answer = np.ones((1,3),dtype=float) 259 | # np.testing.assert_allclose(simplex_x, simplex_x_answer, atol=1e-10) 260 | 261 | import scipy.io as sio 262 | fgm_debug = sio.loadmat('./data/FGM_debug.mat') 263 | H = fgm_debug['H'] 264 | H_ans = fgm_debug['H_ans'] 265 | M = fgm_debug['M'] 266 | J = fgm_debug['J'].ravel() - 1 267 | H_my, _ = fastGrad_simplexProj(M, M[:,J], H, 100) 268 | np.testing.assert_allclose(H_my, H_ans, atol=1e-10) 269 | 270 | def test_snpa(self): 271 | M = np.array([ 272 | [8, 1, 6], 273 | [3, 5, 7], 274 | [4, 9, 2]]) 275 | J, H = snpa(M,3) 276 | J_answer = np.array([1,0,2],dtype=int) 277 | H_answer = np.array([[0,1,0],[1,0,0],[0,0,1]], dtype=float) 278 | np.testing.assert_array_equal(J, J_answer) 279 | np.testing.assert_allclose(H, H_answer, atol=1e-10) 280 | 281 | 282 | if __name__ == '__main__': 283 | unittest.main() 284 | 285 | -------------------------------------------------------------------------------- /spectral_image_NMF.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.stats import norm 3 | import matplotlib.pyplot as plt 4 | from mpl_toolkits.mplot3d import Axes3D 5 | from matplotlib import cm 6 | 7 | from snpa import snpa 8 | 9 | 10 | def create_time_resolved_spectra(): 11 | t = np.linspace(0,50,50) 12 | v = np.linspace(0,40,100) 13 | trace = np.array([np.exp(-t/10), 1-np.exp(-t/10)]).T 14 | spectra = np.array([norm.pdf(v, 10, 1), norm.pdf(v, 25, 3)]).T 15 | spectra3d = trace.dot(spectra.T) 16 | return (t, trace), (v, spectra), spectra3d 17 | 18 | def check_nmf(trspec, nmf_spectra, nmf_trace): 19 | """ 20 | Check if not nan or inf in nmf_spectra and nmf_trace, 21 | and print error of decomposition. 22 | """ 23 | 24 | if not all(np.isfinite(data).all() for data in [nmf_spectra, nmf_trace]): 25 | print('NMF error') 26 | mse, max_relerr = 0, 0 27 | return mse, max_relerr 28 | 29 | reconst_trspec = nmf_trace.dot(nmf_spectra) 30 | mse = np.sqrt(np.mean((reconst_trspec[:] - trspec[:])**2)) 31 | eps = np.max(trspec[:]) * 1e-3 32 | rel_err = np.abs(1 - (reconst_trspec[:]+eps) / (trspec[:]+eps)) 33 | max_relerr = np.max(rel_err) 34 | print('max relative error is %.4f'%max_relerr) 35 | return mse, max_relerr 36 | 37 | def plot_spectra_compare(trspec, v, t, 38 | nmf_spectra, nmf_trace, 39 | ref_trace, time_index): 40 | 41 | fig, axes = plt.subplots(3,1) 42 | 43 | # NMF spectra 44 | axes[0].plot(v, nmf_spectra.T) 45 | legend_str = ['spectra_{nmf-sp %d}'%i 46 | for i in range(nmf_spectra.shape[0])] 47 | axes[0].legend(legend_str) 48 | axes[0].set_title('NMF result') 49 | axes[0].set_xlabel('wavelength') 50 | 51 | # Time-resolved spectra 52 | axes[1].plot(v, trspec[time_index,:].T) 53 | legend_str = ['t = %.1f us'%t[i] for i in time_index] 54 | axes[1].legend(legend_str) 55 | axes[1].set_title('Original spectra') 56 | axes[1].set_xlabel('wavelength') 57 | 58 | # Temporal profile 59 | 60 | scale_ref_trace = ref_trace * \ 61 | (np.max(nmf_trace,axis=0) / np.max(ref_trace,axis=0)) 62 | axes[2].plot(t, nmf_trace) 63 | axes[2].plot(t, scale_ref_trace,'o') 64 | legend_str = ['trace_{nmf-sp %d}'%i 65 | for i in range(nmf_trace.shape[1])] 66 | legend_str.extend(['trace_{ref-peak %d}'%i 67 | for i in range(ref_trace.shape[1])]) 68 | axes[2].legend(legend_str) 69 | axes[2].set_title('comparison of ref_trace and nmf_trace') 70 | axes[2].set_xlabel('time') 71 | axes[2].set_ylabel('intensity') 72 | 73 | plt.subplots_adjust(hspace=0.5) 74 | 75 | return fig, axes 76 | 77 | def plot_trspec(t, v, trspec): 78 | fig = plt.figure() 79 | ax = fig.add_subplot(111, projection='3d') 80 | T, V = np.meshgrid(v, t) 81 | surf = ax.plot_surface(T, V, trspec, cmap=cm.jet) 82 | ax.set_title('time-resolved spectra') 83 | ax.set_xlabel('wavelength') 84 | ax.set_ylabel('time') 85 | ax.set_zlabel('intensity') 86 | fig.colorbar(surf, shrink=0.5, aspect=5) 87 | return ax 88 | 89 | 90 | if __name__ == '__main__': 91 | plt.close('all') 92 | 93 | # Read excel file for spectral data of Criegee. 94 | (t, ref_trace), (v, ref_spectra), trspec = create_time_resolved_spectra() 95 | num_species = ref_spectra.shape[1] 96 | plt_trspec = plot_trspec(t, v, trspec) 97 | 98 | # Execute NMF 99 | [J, nmf_spectra] = snpa(trspec, num_species) 100 | nmf_trace = trspec[:, J] 101 | 102 | # Check NMF and correct by reference 103 | mse = check_nmf(trspec, nmf_spectra, nmf_trace) 104 | rot_spectra = np.linalg.pinv(ref_trace).dot(nmf_trace).dot(nmf_spectra) 105 | 106 | # Plot and save result 107 | time_index = np.linspace(0,trspec.shape[0]-1,4).astype(int) 108 | plot_spectra_compare(trspec,v,t,nmf_spectra,nmf_trace,ref_trace,time_index) 109 | # save_nmf_result(v,nmf_spectra,rot_spectra,t,nmf_trace,ref_trace) 110 | 111 | plt.show() -------------------------------------------------------------------------------- /time_resolved_spectra_NMF.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.stats import norm 3 | import matplotlib.pyplot as plt 4 | from mpl_toolkits.mplot3d import Axes3D 5 | from matplotlib import cm 6 | 7 | from snpa import snpa 8 | 9 | 10 | def create_time_resolved_spectra(): 11 | t = np.linspace(0,50,50) 12 | v = np.linspace(0,40,100) 13 | trace = np.array([np.exp(-t/10), 1-np.exp(-t/10)]).T 14 | spectra = np.array([norm.pdf(v, 10, 1), norm.pdf(v, 25, 3)]).T 15 | spectra3d = trace.dot(spectra.T) 16 | return (t, trace), (v, spectra), spectra3d 17 | 18 | def check_nmf(trspec, nmf_spectra, nmf_trace): 19 | """ 20 | Check if not nan or inf in nmf_spectra and nmf_trace, 21 | and print error of decomposition. 22 | """ 23 | 24 | if not all(np.isfinite(data).all() for data in [nmf_spectra, nmf_trace]): 25 | print('NMF error') 26 | mse, max_relerr = 0, 0 27 | return mse, max_relerr 28 | 29 | reconst_trspec = nmf_trace.dot(nmf_spectra) 30 | mse = np.sqrt(np.mean((reconst_trspec[:] - trspec[:])**2)) 31 | eps = np.max(trspec[:]) * 1e-3 32 | rel_err = np.abs(1 - (reconst_trspec[:]+eps) / (trspec[:]+eps)) 33 | max_relerr = np.max(rel_err) 34 | print('max relative error is %.4f'%max_relerr) 35 | return mse, max_relerr 36 | 37 | def plot_spectra_compare(trspec, v, t, 38 | nmf_spectra, nmf_trace, 39 | ref_trace, time_index): 40 | 41 | fig, axes = plt.subplots(3,1) 42 | 43 | # NMF spectra 44 | axes[0].plot(v, nmf_spectra.T) 45 | legend_str = ['spectra_{nmf-sp %d}'%i 46 | for i in range(nmf_spectra.shape[0])] 47 | axes[0].legend(legend_str) 48 | axes[0].set_title('NMF result') 49 | axes[0].set_xlabel('wavelength / nm') 50 | 51 | # Time-resolved spectra 52 | axes[1].plot(v, trspec[time_index,:].T) 53 | legend_str = ['t = %.1f s'%t[i] for i in time_index] 54 | axes[1].legend(legend_str) 55 | axes[1].set_title('Original spectra') 56 | axes[1].set_xlabel('wavelength / nm') 57 | 58 | # Temporal profile 59 | 60 | scale_ref_trace = ref_trace * \ 61 | (np.max(nmf_trace,axis=0) / np.max(ref_trace,axis=0)) 62 | axes[2].plot(t, nmf_trace) 63 | axes[2].plot(t, scale_ref_trace,'o') 64 | legend_str = ['trace_{nmf-sp %d}'%i 65 | for i in range(nmf_trace.shape[1])] 66 | legend_str.extend(['trace_{ref-peak %d}'%i 67 | for i in range(ref_trace.shape[1])]) 68 | axes[2].legend(legend_str) 69 | axes[2].set_title('comparison of ref_trace and nmf_trace') 70 | axes[2].set_xlabel('time / s') 71 | axes[2].set_ylabel('intensity') 72 | 73 | plt.subplots_adjust(hspace=0.5) 74 | 75 | return fig, axes 76 | 77 | def plot_trspec(trace, spectra, trspec): 78 | 79 | t, ref_trace = trace 80 | v, ref_spectra = spectra 81 | 82 | fig, axes = plt.subplots(2,1) 83 | axes[0].plot(t, ref_trace) 84 | axes[0].set_xlabel('time / s') 85 | axes[0].set_title('artificial trace') 86 | axes[1].plot(v, ref_spectra) 87 | axes[1].set_xlabel('wavenumber / nm') 88 | axes[1].set_title('artificial spectra') 89 | plt.subplots_adjust(hspace=0.5) 90 | 91 | fig = plt.figure() 92 | ax = fig.add_subplot(111, projection='3d') 93 | T, V = np.meshgrid(v, t) 94 | surf = ax.plot_surface(T, V, trspec, cmap=cm.jet) 95 | ax.set_title('artificial time-resolved spectra') 96 | ax.set_xlabel('wavelength / nm') 97 | ax.set_ylabel('time / s') 98 | ax.set_zlabel('intensity') 99 | fig.colorbar(surf, shrink=0.5, aspect=5) 100 | return 101 | 102 | 103 | if __name__ == '__main__': 104 | plt.close('all') 105 | 106 | # Read excel file for spectral data of Criegee. 107 | (t, ref_trace), (v, ref_spectra), trspec = create_time_resolved_spectra() 108 | num_species = ref_spectra.shape[1] 109 | plot_trspec((t, ref_trace), (v, ref_spectra), trspec) 110 | 111 | # Execute NMF 112 | [J, nmf_spectra] = snpa(trspec, num_species) 113 | nmf_trace = trspec[:, J] 114 | 115 | # Check NMF and correct by reference 116 | mse = check_nmf(trspec, nmf_spectra, nmf_trace) 117 | rot_spectra = np.linalg.pinv(ref_trace).dot(nmf_trace).dot(nmf_spectra) 118 | 119 | # Plot and save result 120 | time_index = np.linspace(0,trspec.shape[0]-1,4).astype(int) 121 | plot_spectra_compare(trspec,v,t,nmf_spectra,nmf_trace,ref_trace,time_index) 122 | # save_nmf_result(v,nmf_spectra,rot_spectra,t,nmf_trace,ref_trace) 123 | 124 | plt.show() --------------------------------------------------------------------------------