def cdmd(A, dt = 1, k=None, c=None, sdist='sparse', sf=0.9, p=5, q=2, modes='exact', return_amplitudes=False, return_vandermonde=False, svd='truncated', order=True, trace=True): """ Compressed Dynamic Mode Decomposition. Dynamic Mode Decomposition (DMD) is a data processing algorithm which allows to decompose a matrix `a` in space and time. The matrix `a` is decomposed as `a = FBV`, where the columns of `F` contain the dynamic modes. The modes are ordered corresponding to the amplitudes stored in the diagonal matrix `B`. `V` is a Vandermonde matrix describing the temporal evolution. Parameters ---------- A : array_like Real/complex input matrix `a` with dimensions `(m, n)`. dt : scalar or array_like Factor specifying the time difference between the observations. k : int, optional If `k < (n-1)` low-rank Dynamic Mode Decomposition is computed. c : float, [0,1] Parameter specifying the compression rate. sdist : str `{'unif', 'punif', 'norm', 'sparse', 'spixel'}` Specify the distribution of the sensing matrix `S`. sf : int, optional `sf` sets the sparsity factor for the `sparse` sdist, i.e. `sf=0.9` means `90%` of the sensing matrix `S` entries are zero. p : int, optional `p` sets the oversampling parameter for rSVD (default `k=5`). q : int, optional `q` sets the number of power iterations for rSVD (`default=1`). modes : str `{'exact', 'exact_scaled'}` 'exact' : computes the exact dynamic modes, `F = Y * V * (S**-1) * W`. 'exact_scaled' : computes the exact dynamic modes, `F = (1/l) * Y * V * (S**-1) * W`. return_amplitudes : bool `{True, False}` True: return amplitudes in addition to dynamic modes. return_vandermonde : bool `{True, False}` True: return Vandermonde matrix in addition to dynamic modes and amplitudes. svd : str `{'rsvd', 'partial', 'truncated'}` 'rand' : uses randomized singular value decomposition (default). 'partial' : uses partial singular value decomposition. 'truncated' : uses truncated singular value decomposition. rsvd_type : str `{'standard', 'fast'}` 'standard' : (default) Standard algorithm as described in [1, 2]. 'fast' : Version II algorithm as described in [2]. order : bool `{True, False}` True: return modes sorted. Returns ------- F : array_like Matrix containing the dynamic modes of shape `(m, n-1)` or `(m, k)`. b : array_like 1-D array containing the amplitudes of length `min(n-1, k)`. V : array_like Vandermonde matrix of shape `(n-1, n-1)` or `(k, n-1)`. Notes ----- References ---------- S. L. Brunton, et al. "Compressed Sensing and Dynamic Mode Decomposition" (2013). (available at `arXiv <http://arxiv.org/abs/1312.5186>`_). J. H. Tu, et al. "On Dynamic Mode Decomposition: Theory and Applications" (2013). (available at `arXiv <http://arxiv.org/abs/1312.0041>`_). Examples -------- """ #************************************************************************* #*** Author: N. Benjamin Erichson <*****@*****.**> *** #*** <2015> *** #*** License: BSD 3 clause *** #************************************************************************* #Shape of A m, n = A.shape dat_type = A.dtype if dat_type == np.float32: isreal = True real_type = np.float32 fT = rT elif dat_type == np.float64: isreal = True real_type = np.float64 fT = rT elif dat_type == np.complex64: isreal = False real_type = np.float32 fT = cT elif dat_type == np.complex128: isreal = False real_type = np.float64 fT = cT else: raise ValueError('A.dtype is not supported') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compress #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if c==None: Ac = A else: if trace==True: print "Rows compressed from %0.0f" %m + " to %0.0f" %c if sdist=='unif': S = np.array( np.random.uniform( -1 , 1 , size=( c, m ) ) , dtype = dat_type ) if isreal==False: S += 1j * np.array( np.random.uniform(-1 , 1 , size=( c, m ) ) , dtype = dat_type ) #Compress input matrix Ac = S.dot(A) del(S) elif sdist=='punif': S = np.array( np.random.uniform( 0 , 1 , size=( c, m ) ) , dtype = dat_type ) if isreal==False: S += 1j * np.array( np.random.uniform(0 , 1 , size=( c, m ) ) , dtype = dat_type ) #Compress input matrix Ac = S.dot(A) del(S) elif sdist=='norm': S = np.array( np.random.standard_normal( size=( c, m ) ) , dtype = dat_type ) if isreal==False: S += 1j * np.array( np.random.standard_normal( size=( c, m ) ) , dtype = dat_type ) #Compress input matrix Ac = S.dot(A) del(S) elif sdist=='sparse': density = 1-sf S = sci.sparse.rand(c, m, density=density, format='coo', dtype=dat_type, random_state=None) if isreal==False: S.data += 1j * np.array( np.random.uniform(0 , 1 , size=( len(S.data) ) ) , dtype = dat_type ) if trace==True: print "Sparse: %f" %sf + " zeros " S.data *=2 S.data -=1 S = S.tocsr() #Compress input matrix Ac = S.dot(A) del(S) elif sdist=='spixel': rrows = np.random.choice( np.arange(m), size=c, replace=False, p=None) Ac = np.array( A[ rrows , : ] , dtype = dat_type ) else: raise ValueError('Sampling distribution is not supported.') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Split data into lef and right snapshot sequence #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ X = Ac[ : , xrange( 0 , n-1 ) ] #pointer Y = Ac[ : , xrange( 1 , n ) ] #pointer #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Singular Value Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if k != None: if svd=="rsvd": U, s, Vh = rsvd( X, k=k , p=p , q=q ) elif svd=="partial": U, s, Vh = scislin.svds( X , k=k ) # reverse the n first columns of u U[ : , :k ] = U[ : , k-1::-1 ] # reverse s s = s[ ::-1 ] # reverse the n first rows of vt Vh[ :k , : ] = Vh[ k-1::-1 , : ] elif svd=="truncated": U, s, Vh = sci.linalg.svd( X , compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) U = U[ : , xrange(k) ] s = s[ xrange(k) ] Vh = Vh[ xrange(k) , : ] else: raise ValueError('SVD algorithm is not supported.') else: U, s, Vh = sci.linalg.svd( X , compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) #EndIf #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Solve the LS problem to find estimate for M using the pseudo-inverse #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #real: M = U.T * Y * Vt.T * S**-1 #complex: M = U.H * Y * Vt.H * S**-1 #Let G = Y * Vt.H * S**-1, hence M = M * G Vscaled = fT(Vh) * s**-1 G = np.dot( Y , Vscaled ) M = np.dot( fT(U) , G ) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Eigen Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ l, W = sci.linalg.eig( M , right=True, overwrite_a=True ) omega = np.log(l) / dt #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Order #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if order==True: sort_idx = np.argsort(np.abs(omega)) W = W[ :, sort_idx ] l = l[ sort_idx ] omega = omega[ sort_idx ] #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute DMD Modes #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if modes=='exact': F = np.dot( A[ : , xrange( 1 , n ) ] , np.dot( Vscaled , W ) ) elif modes=='exact_scaled': F = np.dot( A[ : , xrange( 1 , n ) ] , np.dot( Vscaled , W ) ) * ( 1/l ) else: raise ValueError('Type of modes is not supported, choose "exact" or "standard".') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute amplitueds b using least-squares: Fb=x1 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes==True: b , _ , _ , _ = sci.linalg.lstsq( F , A[ : , 0 ]) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute Vandermonde matrix (CPU) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_vandermonde==True: V = np.fliplr(np.vander( l , N = n )) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Return #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes==True and return_vandermonde==True: return F, b, V, omega elif return_amplitudes==True and return_vandermonde==False: return F, b, omega else: return F , omega
if __name__ == '__main__': fig, axarr = plt.subplots(1, 4) fig.set_size_inches(15, 4) A = face(gray=True) imwrite('figures/raccoon.jpg', A.astype(np.uint8)) rank = 15 U, S, Vt = np.linalg.svd(A) U, S, Vt = U[:, :rank], S[:rank], Vt[:rank, :] M_recon = (U * S) @ Vt fname = 'figures/raccoon_actual_k%s.jpg' % rank imwrite(fname, M_recon.astype(np.uint8)) axarr[0].imshow(M_recon, cmap='gray') axarr[0].axis('off') axarr[0].set_title(r'SVD') for i, l in enumerate([1, 10, 100]): U, S, Vt = rsvd(A, rank, l, n_subspace_iters=0) M_recon = (U * S) @ Vt fname = 'figures/raccoon_k%s_p%s.jpg' % (rank, l) imwrite(fname, M_recon.astype(np.uint8)) axarr[i + 1].imshow(M_recon, cmap='gray') axarr[i + 1].axis('off') axarr[i + 1].set_title(r'RSVD, $\ell = %s$' % l) # plt.title('foo') plt.tight_layout() plt.show()
def dmd(A, dt = 1, k=None, p=5, q=2, modes='exact', return_amplitudes=False, return_vandermonde=False, svd='truncated', sdist='unif', order=True): """ Dynamic Mode Decomposition. Dynamic Mode Decomposition (DMD) is a data processing algorithm which allows to decompose a matrix `a` in space and time. The matrix `a` is decomposed as `a = FBV`, where the columns of `F` contain the dynamic modes. The modes are ordered corresponding to the amplitudes stored in the diagonal matrix `B`. `V` is a Vandermonde matrix describing the temporal evolution. Parameters ---------- A : array_like Real/complex input matrix `a` with dimensions `(m, n)`. dt : scalar or array_like Factor specifying the time difference between the observations. k : int, optional If `k < (n-1)` low-rank Dynamic Mode Decomposition is computed. p : int, optional `p` sets the oversampling parameter for rSVD (default `p=5`). q : int, optional `q` sets the number of power iterations for rSVD (default `q=1`). modes : str `{'standard', 'exact', 'exact_scaled'}` 'standard' : uses the standard definition to compute the dynamic modes, `F = U * W`. 'exact' : computes the exact dynamic modes, `F = Y * V * (S**-1) * W`. 'exact_scaled' : computes the exact dynamic modes, `F = (1/l) * Y * V * (S**-1) * W`. return_amplitudes : bool `{True, False}` True: return amplitudes in addition to dynamic modes. return_vandermonde : bool `{True, False}` True: return Vandermonde matrix in addition to dynamic modes and amplitudes. svd : str `{'rsvd', 'partial', 'truncated'}` 'rsvd' : uses randomized singular value decomposition (default). 'partial' : uses partial singular value decomposition. 'truncated' : uses truncated singular value decomposition. sdist : str `{'unif', 'norm'}` 'unif' : Uniform `[-1,1]`. 'norm' : Normal `~N(0,1)`. order : bool `{True, False}` True: return modes sorted. Returns ------- F : array_like Matrix containing the dynamic modes of shape `(m, n-1)` or `(m, k)`. b : array_like, optional 1-D array containing the amplitudes of length `min(n-1, k)`. V : array_like, optional Vandermonde matrix of shape `(n-1, n-1)` or `(k, n-1)`. omega : array_like Time scaled eigenvalues: `ln(l)/dt`. Notes ----- References ---------- J. H. Tu, et al. "On Dynamic Mode Decomposition: Theory and Applications" (2013). (available at `arXiv <http://arxiv.org/abs/1312.0041>`_). N. B. Erichson and C. Donovan. "Randomized Low-Rank Dynamic Mode Decomposition for Motion Detection" (2015). Under Review. Examples -------- >>> #Numpy >>> import numpy as np >>> #DMD >>> from skrla import dmd >>> #Plot libs >>> import matplotlib.pyplot as plt >>> from mpl_toolkits.mplot3d import Axes3D >>> from matplotlib import cm >>> # >>> # Create an artifical data-set: >>> # >>> # Define time and space discretizations >>> x=np.linspace( -15, 15, 200) >>> t=np.linspace(0, 8*np.pi , 80) >>> dt=t[2]-t[1] >>> X, T = np.meshgrid(x,t) >>> # Create two patio-temporal patterns >>> F1 = 0.5* np.cos(X)*(1.+0.* T) >>> F2 = ( (1./np.cosh(X)) * np.tanh(X)) *(2.*np.exp(1j*2.8*T)) >>> # Add both signals >>> F = (F1+F2) >>> #Plot dataset >>> fig = plt.figure() >>> ax = fig.add_subplot(231, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=True) >>> ax.set_zlim(-1, 1) >>> plt.title('F') >>> ax = fig.add_subplot(232, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F1, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F1') >>> ax = fig.add_subplot(233, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F2, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F2') >>> #Dynamic Mode Decomposition of F >>> F_gpu = np.array(F.T, np.complex64, order='F') >>> F_gpu = gpuarray.to_gpu(F_gpu) >>> Fmodes, b, V, omega = dmd(F, k=2, modes='exact', return_amplitudes=True, return_vandermonde=True) >>> omega = omega_gpu.get() >>> #Reconstruct the original signal >>> plt.scatter(omega.real, omega.imag, marker='o', c='r') >>> F1tilde = np.dot(Fmodes[:,0:1] , np.dot(b[0], V[0:1,:] ) ) >>> F2tilde = np.dot(Fmodes[:,1:2] , np.dot(b[1], V[1:2,:] ) ) >>> #Plot DMD modes >>> #Mode 0 >>> ax = fig.add_subplot(235, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X[0:F1tilde.shape[1],:], T[0:F1tilde.shape[1],:], F1tilde.T, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F1_tilde') >>> #Mode 1 >>> ax = fig.add_subplot(236, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X[0:F2tilde.shape[1],:], T[0:F2tilde.shape[1],:], F2tilde.T, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F2_tilde') >>> plt.show() """ #************************************************************************* #*** Author: N. Benjamin Erichson <*****@*****.**> *** #*** <2015> *** #*** License: BSD 3 clause *** #************************************************************************* #Shape of D m, n = A.shape dat_type = A.dtype if dat_type == np.float32: isreal = True real_type = np.float32 fT = rT elif dat_type == np.float64: isreal = True real_type = np.float64 fT = rT elif dat_type == np.complex64: isreal = False real_type = np.float32 fT = cT elif dat_type == np.complex128: isreal = False real_type = np.float64 fT = cT else: raise ValueError('A.dtype is not supported') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Split data into lef and right snapshot sequence #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ X = A[ : , xrange( 0 , n-1 ) ] #pointer Y = A[ : , xrange( 1 , n ) ] #pointer #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Singular Value Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if k != None: if svd=="rsvd": U, s, Vh = rsvd( X, k=k , p=p , q=q , sdist=sdist) elif svd=="partial": U, s, Vh = scislin.svds( X , k=k ) # reverse the n first columns of u U[ : , :k ] = U[ : , k-1::-1 ] # reverse s s = s[ ::-1 ] # reverse the n first rows of vt Vh[ :k , : ] = Vh[ k-1::-1 , : ] elif svd=="truncated": U, s, Vh = sci.linalg.svd( X , compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) U = U[ : , xrange(k) ] s = s[ xrange(k) ] Vh = Vh[ xrange(k) , : ] else: raise ValueError('SVD algorithm is not supported.') else: U, s, Vh = sci.linalg.svd( X , compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) #k = optht(X, s) #print('Optimal hard threshold: ', k) #U = U[ : , xrange(k) ] #s = s[ xrange(k) ] #Vh = Vh[ xrange(k) , : ] #EndIf #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Solve the LS problem to find estimate for M using the pseudo-inverse #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #real: M = U.T * Y * Vt.T * S**-1 #complex: M = U.H * Y * Vt.H * S**-1 #Let G = Y * Vt.H * S**-1, hence M = M * G Vscaled = fT(Vh) * s**-1 G = np.dot( Y , Vscaled ) M = np.dot( fT(U), G ) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Eigen Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ l, W = sci.linalg.eig( M , right=True, overwrite_a=True ) omega = np.log(l) / dt #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Order #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if order==True: sort_idx = np.argsort(np.abs(omega)) W = W[ :, sort_idx ] l = l[ sort_idx ] omega = omega[ sort_idx ] #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute DMD Modes #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if modes=='standard': F = np.dot( U , W ) elif modes=='exact': F = np.dot( G , W ) elif modes=='exact_scaled': F = np.dot((1/l) * G , W ) else: raise ValueError('Type of modes is not supported, choose "exact" or "standard".') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute amplitueds b using least-squares: Fb=x1 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes==True: b , _ , _ , _ = sci.linalg.lstsq( F , A[ : , 0 ]) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute Vandermonde matrix #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_vandermonde==True: V = np.fliplr(np.vander( l , N = n )) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Return #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes==True and return_vandermonde==True: return F, b, V, omega elif return_amplitudes==True and return_vandermonde==False: return F, b, omega else: return F, omega, s
def dmd(A, dt=1, k=None, p=5, q=2, modes='exact', return_amplitudes=False, return_vandermonde=False, svd='truncated', sdist='uniform', order=True): """ Dynamic Mode Decomposition. Dynamic Mode Decomposition (DMD) is a data processing algorithm which allows to decompose a matrix `A` in space and time. The matrix `A` is decomposed as `A = F * B * V`, where the columns of `F` contain the dynamic modes. The modes are ordered corresponding to the amplitudes stored in the diagonal matrix `B`. `V` is a Vandermonde matrix describing the temporal evolution. Parameters ---------- A : array_like Real/complex input matrix `a` with dimensions `(m, n)`. dt : scalar or array_like Factor specifying the time difference between the observations. k : int, optional If `k < (n-1)` low-rank Dynamic Mode Decomposition is computed. p : int, optional `p` sets the oversampling parameter for rSVD (default `p=5`). q : int, optional `q` sets the number of power iterations for rSVD (default `q=1`). modes : str `{'standard', 'exact', 'exact_scaled'}` 'standard' : uses the standard definition to compute the dynamic modes, `F = U * W`. 'exact' : computes the exact dynamic modes, `F = Y * V * (S**-1) * W`. 'exact_scaled' : computes the exact dynamic modes, `F = (1/l) * Y * V * (S**-1) * W`. return_amplitudes : bool `{True, False}` True: return amplitudes in addition to dynamic modes. return_vandermonde : bool `{True, False}` True: return Vandermonde matrix in addition to dynamic modes and amplitudes. svd : str `{'rsvd', 'partial', 'truncated'}` 'rsvd' : uses randomized singular value decomposition (default). 'partial' : uses partial singular value decomposition. 'truncated' : uses truncated singular value decomposition. sdist : str `{'uniform', 'normal'}` 'uniform' : Uniform `[-1,1]`. 'normal' : Normal `~N(0,1)`. order : bool `{True, False}` True: return modes sorted. Returns ------- F : array_like Matrix containing the dynamic modes of shape `(m, n-1)` or `(m, k)`. b : array_like, if `return_amplitudes=True` 1-D array containing the amplitudes of length `min(n-1, k)`. V : array_like, if `return_vandermonde=True` Vandermonde matrix of shape `(n-1, n-1)` or `(k, n-1)`. omega : array_like Time scaled eigenvalues: `ln(l)/dt`. Notes ----- References ---------- J. H. Tu, et al. "On Dynamic Mode Decomposition: Theory and Applications" (2013). (available at `arXiv <http://arxiv.org/abs/1312.0041>`_). N. B. Erichson and C. Donovan. "Randomized Low-Rank Dynamic Mode Decomposition for Motion Detection" (2015). Under Review. Examples -------- >>> #Numpy >>> import numpy as np >>> #DMD >>> from skrla import dmd >>> #Plot libs >>> import matplotlib.pyplot as plt >>> from mpl_toolkits.mplot3d import Axes3D >>> from matplotlib import cm >>> # >>> # Create an artifical data-set: >>> # >>> # Define time and space discretizations >>> x=np.linspace( -15, 15, 200) >>> t=np.linspace(0, 8*np.pi , 80) >>> dt=t[2]-t[1] >>> X, T = np.meshgrid(x,t) >>> # Create two patio-temporal patterns >>> F1 = 0.5* np.cos(X)*(1.+0.* T) >>> F2 = ( (1./np.cosh(X)) * np.tanh(X)) *(2.*np.exp(1j*2.8*T)) >>> # Add both signals >>> F = (F1+F2) >>> #Plot dataset >>> fig = plt.figure() >>> ax = fig.add_subplot(231, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=True) >>> ax.set_zlim(-1, 1) >>> plt.title('F') >>> ax = fig.add_subplot(232, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F1, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F1') >>> ax = fig.add_subplot(233, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X, T, F2, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F2') >>> #Dynamic Mode Decomposition of F >>> F_gpu = np.array(F.T, np.complex64, order='F') >>> F_gpu = gpuarray.to_gpu(F_gpu) >>> Fmodes, b, V, omega = dmd(F, k=2, modes='exact', return_amplitudes=True, return_vandermonde=True) >>> omega = omega_gpu.get() >>> #Reconstruct the original signal >>> plt.scatter(omega.real, omega.imag, marker='o', c='r') >>> F1tilde = np.dot(Fmodes[:,0:1] , np.dot(b[0], V[0:1,:] ) ) >>> F2tilde = np.dot(Fmodes[:,1:2] , np.dot(b[1], V[1:2,:] ) ) >>> #Plot DMD modes >>> #Mode 0 >>> ax = fig.add_subplot(235, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X[0:F1tilde.shape[1],:], T[0:F1tilde.shape[1],:], F1tilde.T, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F1_tilde') >>> #Mode 1 >>> ax = fig.add_subplot(236, projection='3d') >>> ax = fig.gca(projection='3d') >>> surf = ax.plot_surface(X[0:F2tilde.shape[1],:], T[0:F2tilde.shape[1],:], F2tilde.T, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False) >>> ax.set_zlim(-1, 1) >>> plt.title('F2_tilde') >>> plt.show() """ #************************************************************************* #*** Author: N. Benjamin Erichson <*****@*****.**> *** #*** <2015> *** #*** License: BSD 3 clause *** #************************************************************************* #Shape of D m, n = A.shape dat_type = A.dtype if dat_type == np.float32: isreal = True real_type = np.float32 fT = rT elif dat_type == np.float64: isreal = True real_type = np.float64 fT = rT elif dat_type == np.complex64: isreal = False real_type = np.float32 fT = cT elif dat_type == np.complex128: isreal = False real_type = np.float64 fT = cT else: raise ValueError('A.dtype is not supported') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Split data into lef and right snapshot sequence #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ X = A[:, xrange(0, n - 1)] #pointer Y = A[:, xrange(1, n)] #pointer #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Singular Value Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if k != None: if svd == "rsvd": U, s, Vh = rsvd(X, k=k, p=p, q=q, sdist=sdist) elif svd == "partial": U, s, Vh = scislin.svds(X, k=k) # reverse the n first columns of u U[:, :k] = U[:, k - 1::-1] # reverse s s = s[::-1] # reverse the n first rows of vt Vh[:k, :] = Vh[k - 1::-1, :] elif svd == "truncated": U, s, Vh = sci.linalg.svd(X, compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) U = U[:, xrange(k)] s = s[xrange(k)] Vh = Vh[xrange(k), :] else: raise ValueError('SVD algorithm is not supported.') else: U, s, Vh = sci.linalg.svd(X, compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) #EndIf #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Solve the LS problem to find estimate for M using the pseudo-inverse #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #real: M = U.T * Y * Vt.T * S**-1 #complex: M = U.H * Y * Vt.H * S**-1 #Let G = Y * Vt.H * S**-1, hence M = M * G Vscaled = fT(Vh) * s**-1 G = np.dot(Y, Vscaled) M = np.dot(fT(U), G) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Eigen Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ l, W = sci.linalg.eig(M, right=True, overwrite_a=True) omega = np.log(l) / dt #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Order #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if order == True: sort_idx = np.argsort(np.abs(omega)) W = W[:, sort_idx] l = l[sort_idx] omega = omega[sort_idx] #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute DMD Modes #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if modes == 'standard': F = np.dot(U, W) elif modes == 'exact': F = np.dot(G, W) elif modes == 'exact_scaled': F = np.dot((1 / l) * G, W) else: raise ValueError( 'Type of modes is not supported, choose "exact" or "standard".') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute amplitueds b using least-squares: Fb=x1 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes == True: b, _, _, _ = sci.linalg.lstsq(F, A[:, 0]) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute Vandermonde matrix #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_vandermonde == True: V = np.fliplr(np.vander(l, N=n)) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Return #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes == True and return_vandermonde == True: return F, b, V, omega elif return_amplitudes == True and return_vandermonde == False: return F, b, omega else: return F, omega, s
def cdmd(A, dt=1, k=None, c=None, sdist='sparse', sf=0.9, p=5, q=2, modes='exact', return_amplitudes=False, return_vandermonde=False, svd='truncated', order=True, trace=True): """ Compressed Dynamic Mode Decomposition. Dynamic Mode Decomposition (DMD) is a data processing algorithm which allows to decompose a matrix `a` in space and time. The matrix `a` is decomposed as `a = FBV`, where the columns of `F` contain the dynamic modes. The modes are ordered corresponding to the amplitudes stored in the diagonal matrix `B`. `V` is a Vandermonde matrix describing the temporal evolution. Parameters ---------- A : array_like Real/complex input matrix `a` with dimensions `(m, n)`. dt : scalar or array_like Factor specifying the time difference between the observations. k : int, optional If `k < (n-1)` low-rank Dynamic Mode Decomposition is computed. c : float, [0,1] Parameter specifying the compression rate. sdist : str `{'unif', 'punif', 'norm', 'sparse', 'spixel'}` Specify the distribution of the sensing matrix `S`. sf : int, optional `sf` sets the sparsity factor for the `sparse` sdist, i.e. `sf=0.9` means `90%` of the sensing matrix `S` entries are zero. p : int, optional `p` sets the oversampling parameter for rSVD (default `k=5`). q : int, optional `q` sets the number of power iterations for rSVD (`default=1`). modes : str `{'exact', 'exact_scaled'}` 'exact' : computes the exact dynamic modes, `F = Y * V * (S**-1) * W`. 'exact_scaled' : computes the exact dynamic modes, `F = (1/l) * Y * V * (S**-1) * W`. return_amplitudes : bool `{True, False}` True: return amplitudes in addition to dynamic modes. return_vandermonde : bool `{True, False}` True: return Vandermonde matrix in addition to dynamic modes and amplitudes. svd : str `{'rsvd', 'partial', 'truncated'}` 'rand' : uses randomized singular value decomposition (default). 'partial' : uses partial singular value decomposition. 'truncated' : uses truncated singular value decomposition. rsvd_type : str `{'standard', 'fast'}` 'standard' : (default) Standard algorithm as described in [1, 2]. 'fast' : Version II algorithm as described in [2]. order : bool `{True, False}` True: return modes sorted. Returns ------- F : array_like Matrix containing the dynamic modes of shape `(m, n-1)` or `(m, k)`. b : array_like 1-D array containing the amplitudes of length `min(n-1, k)`. V : array_like Vandermonde matrix of shape `(n-1, n-1)` or `(k, n-1)`. Notes ----- References ---------- S. L. Brunton, et al. "Compressed Sensing and Dynamic Mode Decomposition" (2013). (available at `arXiv <http://arxiv.org/abs/1312.5186>`_). J. H. Tu, et al. "On Dynamic Mode Decomposition: Theory and Applications" (2013). (available at `arXiv <http://arxiv.org/abs/1312.0041>`_). Examples -------- """ #************************************************************************* #*** Author: N. Benjamin Erichson <*****@*****.**> *** #*** <2015> *** #*** License: BSD 3 clause *** #************************************************************************* #Shape of A m, n = A.shape dat_type = A.dtype if dat_type == np.float32: isreal = True real_type = np.float32 fT = rT elif dat_type == np.float64: isreal = True real_type = np.float64 fT = rT elif dat_type == np.complex64: isreal = False real_type = np.float32 fT = cT elif dat_type == np.complex128: isreal = False real_type = np.float64 fT = cT else: raise ValueError('A.dtype is not supported') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compress #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if c == None: Ac = A else: if trace == True: print "Rows compressed from %0.0f" % m + " to %0.0f" % c if sdist == 'unif': S = np.array(np.random.uniform(-1, 1, size=(c, m)), dtype=dat_type) if isreal == False: S += 1j * np.array(np.random.uniform(-1, 1, size=(c, m)), dtype=dat_type) #Compress input matrix Ac = S.dot(A) del (S) elif sdist == 'punif': S = np.array(np.random.uniform(0, 1, size=(c, m)), dtype=dat_type) if isreal == False: S += 1j * np.array(np.random.uniform(0, 1, size=(c, m)), dtype=dat_type) #Compress input matrix Ac = S.dot(A) del (S) elif sdist == 'norm': S = np.array(np.random.standard_normal(size=(c, m)), dtype=dat_type) if isreal == False: S += 1j * np.array(np.random.standard_normal(size=(c, m)), dtype=dat_type) #Compress input matrix Ac = S.dot(A) del (S) elif sdist == 'sparse': density = 1 - sf S = sci.sparse.rand(c, m, density=density, format='coo', dtype=dat_type, random_state=None) if isreal == False: S.data += 1j * np.array(np.random.uniform( 0, 1, size=(len(S.data))), dtype=dat_type) if trace == True: print "Sparse: %f" % sf + " zeros " S.data *= 2 S.data -= 1 S = S.tocsr() #Compress input matrix Ac = S.dot(A) del (S) elif sdist == 'spixel': rrows = np.random.choice(np.arange(m), size=c, replace=False, p=None) Ac = np.array(A[rrows, :], dtype=dat_type) else: raise ValueError('Sampling distribution is not supported.') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Split data into lef and right snapshot sequence #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ X = Ac[:, xrange(0, n - 1)] #pointer Y = Ac[:, xrange(1, n)] #pointer #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Singular Value Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if k != None: if svd == "rsvd": U, s, Vh = rsvd(X, k=k, p=p, q=q) elif svd == "partial": U, s, Vh = scislin.svds(X, k=k) # reverse the n first columns of u U[:, :k] = U[:, k - 1::-1] # reverse s s = s[::-1] # reverse the n first rows of vt Vh[:k, :] = Vh[k - 1::-1, :] elif svd == "truncated": U, s, Vh = sci.linalg.svd(X, compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) U = U[:, xrange(k)] s = s[xrange(k)] Vh = Vh[xrange(k), :] else: raise ValueError('SVD algorithm is not supported.') else: U, s, Vh = sci.linalg.svd(X, compute_uv=True, full_matrices=False, overwrite_a=False, check_finite=True) #EndIf #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Solve the LS problem to find estimate for M using the pseudo-inverse #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #real: M = U.T * Y * Vt.T * S**-1 #complex: M = U.H * Y * Vt.H * S**-1 #Let G = Y * Vt.H * S**-1, hence M = M * G Vscaled = fT(Vh) * s**-1 G = np.dot(Y, Vscaled) M = np.dot(fT(U), G) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Eigen Decomposition #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ l, W = sci.linalg.eig(M, right=True, overwrite_a=True) omega = np.log(l) / dt #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Order #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if order == True: sort_idx = np.argsort(np.abs(omega)) W = W[:, sort_idx] l = l[sort_idx] omega = omega[sort_idx] #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute DMD Modes #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if modes == 'exact': F = np.dot(A[:, xrange(1, n)], np.dot(Vscaled, W)) elif modes == 'exact_scaled': F = np.dot(A[:, xrange(1, n)], np.dot(Vscaled, W)) * (1 / l) else: raise ValueError( 'Type of modes is not supported, choose "exact" or "standard".') #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute amplitueds b using least-squares: Fb=x1 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes == True: b, _, _, _ = sci.linalg.lstsq(F, A[:, 0]) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Compute Vandermonde matrix (CPU) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_vandermonde == True: V = np.fliplr(np.vander(l, N=n)) #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Return #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if return_amplitudes == True and return_vandermonde == True: return F, b, V, omega elif return_amplitudes == True and return_vandermonde == False: return F, b, omega else: return F, omega
# ------------------------------------------------------------------------------ if __name__ == '__main__': fig, ax = plt.subplots(1, 1) fig.set_size_inches(15, 5) m = 200 n = 200 A = generate_decaying_matrix(m, n) mins = [] errs = [] ls = range(1, 151, 5) for l in ls: _, _, _, Q = rsvd(A, l, n_oversamples=0, return_range=True) err = np.linalg.norm((np.eye(m) - Q @ Q.T) @ A, 2) errs.append(np.log10(err)) S = np.linalg.svd(A, compute_uv=False) min_ = S[l + 1] mins.append(np.log10(min_)) ax.scatter(ls, mins, color='#11accd', s=30, label=r'$\log_{10}(\sigma_{\ell+1})$', marker='v') ax.scatter(ls, errs,
} ls = [20, 40, 60, 80, 100] qs = [0, 1, 2, 3] for l in ls: # Compute theoretical minimum error for each l. S = np.linalg.svd(A, compute_uv=False) min_ = S[l + 1] mins.append(min_) # Compute error for each number of subspace iterations. for q in qs: _, _, _, Q = rsvd(A, l, n_oversamples=0, n_subspace_iters=q, return_range=True) err = np.linalg.norm((np.eye(m) - Q @ Q.T) @ A, 2) q2list[q][0].append(err) ax.scatter(ls, mins, color='gray', s=30, label='Minimum error', marker='s') ax.plot(ls, mins, color='gray', linewidth=1) for q in qs: data, color, marker = q2list[q] ax.scatter(ls, data, s=30, label=r'$q = %s$' % q, marker=marker,