def compute_model(self, Markovs, num_states, mc=None, mo=None): """Computes the A, B, and C LTI ROM matrices. Args: ``Markovs``: Array of Markov params w/indices [time, output, input] ``Markovs[i]`` is the Markov parameter C A**i B. ``num_states``: Number of states to be found for the model. Kwargs: ``mc``: Number of Markov parameters for controllable dimension. ``mo``: Number of Markov parameters for observable dimension. Default is mc and mo equal and maximal for a balanced model. Assembles the Hankel matrices from self.Markovs and takes SVD. Default values of ``mc`` and ``mo`` are equal and maximal for a balanced model. Tip: For discrete time systems the impulse is applied over a time interval dt and so has a time-integral 1*dt rather than 1. This means the reduced B matrix is "off" by a factor of dt. You can account for this by multiplying B by dt. """ #SVD is ``L_sing_vecs*N.mat(N.diag(sing_vals))*\ # R_sing_vecs.H = Hankel_mat`` self._set_Markovs(Markovs) self.mc = mc self.mo = mo self._assemble_Hankel() self.L_sing_vecs, self.sing_vals, self.R_sing_vecs = \ util.svd(self.Hankel_mat) # Truncate matrices Ur = N.mat(self.L_sing_vecs[:, :num_states]) Er = N.squeeze(self.sing_vals[:num_states]) Vr = N.mat(self.R_sing_vecs[:, :num_states]) self.A = N.mat(N.diag(Er**-.5)) * Ur.H * self.Hankel_mat2 * Vr * \ N.mat(N.diag(Er**-.5)) self.B = (N.mat(N.diag(Er**.5)) * (Vr.H)[:, :self.num_inputs]) # *dt above is removed, users must do this themselves. # It is explained in the docs. self.C = Ur[:self.num_Markovs] * N.mat(N.diag(Er**.5)) if (N.abs(N.linalg.eigvals(self.A)) >= 1.).any() and self.verbosity: print 'Warning: Unstable eigenvalues of reduced A matrix' print 'eig vals are', N.linalg.eigvals(self.A) return self.A, self.B, self.C
def _helper_compute_DMD_from_data(self, vecs, adv_vecs, inner_product): correlation_mat = inner_product(vecs, vecs) W, Sigma, dummy = util.svd(correlation_mat) # dummy = W. U = vecs.dot(W).dot(N.diag(Sigma**-0.5)) ritz_vals, eig_vecs = N.linalg.eig(inner_product( U, adv_vecs).dot(W).dot(N.diag(Sigma**-0.5))) eig_vecs = N.mat(eig_vecs) ritz_vecs = U.dot(eig_vecs) scaling = N.linalg.lstsq(ritz_vecs, vecs[:, 0])[0] scaling = N.mat(N.diag(N.array(scaling).squeeze())) ritz_vecs = ritz_vecs.dot(scaling) build_coeffs = W.dot(N.diag(Sigma**-0.5)).dot(eig_vecs).dot(scaling) mode_norms = N.diag(inner_product(ritz_vecs, ritz_vecs)).real return ritz_vals, ritz_vecs, build_coeffs, mode_norms
def test_svd(self): num_internals_list = [10, 50] num_rows_list = [3, 5, 40] num_cols_list = [1, 9, 70] for num_rows in num_rows_list: for num_cols in num_cols_list: for num_internals in num_internals_list: left_mat = N.mat(N.random.random((num_rows, num_internals))) right_mat = N.mat(N.random.random((num_internals, num_cols))) full_mat = left_mat*right_mat L_sing_vecs, sing_vals, R_sing_vecs = util.svd(full_mat) U, E, V_comp_conj = N.linalg.svd(full_mat, full_matrices=0) V = N.mat(V_comp_conj).H if num_internals < num_rows or num_internals < num_cols: U = U[:,:num_internals] V = V[:,:num_internals] E = E[:num_internals] N.testing.assert_allclose(L_sing_vecs, U) N.testing.assert_allclose(sing_vals, E) N.testing.assert_allclose(R_sing_vecs, V)
def _helper_compute_DMD_from_data(self, vec_array, adv_vec_array, inner_product): # Create lists of vecs, advanced vecs for inner product function vecs = [vec_array[:, i] for i in range(vec_array.shape[1])] adv_vecs = [adv_vec_array[:, i] for i in range(adv_vec_array.shape[1])] # Compute DMD correlation_mat = inner_product(vecs, vecs) W, Sigma, dummy = util.svd(correlation_mat) # dummy = W. U = vec_array.dot(W).dot(N.diag(Sigma**-0.5)) U_list = [U[:,i] for i in range(U.shape[1])] ritz_vals, eig_vecs = N.linalg.eig(inner_product( U_list, adv_vecs).dot(W).dot(N.diag(Sigma**-0.5))) eig_vecs = N.mat(eig_vecs) ritz_vecs = U.dot(eig_vecs) scaling = N.linalg.lstsq(ritz_vecs, vec_array[:, 0])[0] scaling = N.mat(N.diag(N.array(scaling).squeeze())) ritz_vecs = ritz_vecs.dot(scaling) build_coeffs = W.dot(N.diag(Sigma**-0.5)).dot(eig_vecs).dot(scaling) ritz_vecs_list = [N.array(ritz_vecs[:,i]).squeeze() for i in range(ritz_vecs.shape[1])] mode_norms = N.diag(inner_product(ritz_vecs_list, ritz_vecs_list)).real return ritz_vals, ritz_vecs, build_coeffs, mode_norms
def compute_DMD_matrices_direct_method(vecs, mode_indices, adv_vecs=None, inner_product_weights=None, return_all=False): """Dynamic Mode Decomposition/Koopman Mode Decomposition with data in a matrix, using a direct method. Args: ``vecs``: Matrix with vectors as columns. ``mode_indices``: List of mode numbers, ``range(10)`` or ``[3, 0, 5]``. Kwargs: ``adv_vecs``: Matrix with ``vecs`` advanced in time as columns. If not provided, then it is assumed that the vectors are a sequential time-series. Thus ``vecs`` becomes ``vecs[:-1]`` and ``adv_vecs`` becomes ``vecs[1:]``. ``inner_product_weights``: 1D or Matrix of inner product weights. It corresponds to :math:`W` in inner product :math:`v_1^* W v_2`. ``return_all``: Return more objects, see below. Default is false. Returns: ``modes``: Matrix with requested modes as columns. ``ritz_vals``: 1D array of Ritz values. ``mode_norms``: 1D array of mode norms. If ``return_all`` is true, also returns: ``build_coeffs``: Matrix of build coefficients for modes. This method does not square the matrix of vectors as in the method of snapshots (:py:func:`compute_DMD_matrices_snaps_method`). It's slightly more accurate, but slower when the number of elements in a vector is more than the number of vectors (more rows than columns in ``vecs``). """ if _parallel.is_distributed(): raise RuntimeError('Cannot run in parallel.') vec_space = VectorSpaceMatrices(weights=inner_product_weights) vecs = util.make_mat(vecs) if adv_vecs is not None: adv_vecs = util.make_mat(adv_vecs) if inner_product_weights is None: vecs_weighted = vecs if adv_vecs is not None: adv_vecs_weighted = adv_vecs elif inner_product_weights.ndim == 1: sqrt_weights = N.mat(N.diag(inner_product_weights**0.5)) vecs_weighted = sqrt_weights * vecs if adv_vecs is not None: adv_vecs_weighted = sqrt_weights * adv_vecs elif inner_product_weights.ndim == 2: if inner_product_weights.shape[0] > 500: print 'Warning: Cholesky decomposition could be time consuming.' sqrt_weights = N.mat(N.linalg.cholesky(inner_product_weights)).H vecs_weighted = sqrt_weights * vecs if adv_vecs is not None: adv_vecs_weighted = sqrt_weights * adv_vecs # Compute low-order linear map for sequential snapshot set. This takes # advantage of the fact that for a sequential dataset, the unadvanced # and advanced vectors overlap. if adv_vecs is None: U, sing_vals, correlation_mat_evecs = util.svd(vecs_weighted[:, :-1]) correlation_mat_evals = sing_vals**2 correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H last_col = U.H * vecs_weighted[:, -1] correlation_mat_evals_sqrt = N.mat(N.diag(sing_vals**-1.0)) correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H low_order_linear_map = N.mat(N.concatenate( (correlation_mat_evals_sqrt * correlation_mat_evecs.H * \ correlation_mat[:, 1:], last_col), axis=1)) * \ correlation_mat_evecs * correlation_mat_evals_sqrt else: if vecs.shape != adv_vecs.shape: raise ValueError(('vecs and adv_vecs are not the same shape.')) U, sing_vals, correlation_mat_evecs = util.svd(vecs_weighted) correlation_mat_evals_sqrt = N.mat(N.diag(sing_vals**-1.0)) low_order_linear_map = U.H * adv_vecs_weighted * \ correlation_mat_evecs * correlation_mat_evals_sqrt correlation_mat_evals = sing_vals**2 correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H # Compute eigendecomposition of low-order linear map. ritz_vals, low_order_evecs = N.linalg.eig(low_order_linear_map) build_coeffs = correlation_mat_evecs *\ correlation_mat_evals_sqrt * low_order_evecs *\ N.diag(N.array(N.array(N.linalg.inv( low_order_evecs.H * low_order_evecs) * low_order_evecs.H *\ correlation_mat_evals_sqrt * correlation_mat_evecs.H * correlation_mat[:, 0]).squeeze(), ndmin=1)) mode_norms = N.diag(build_coeffs.H * correlation_mat * build_coeffs).real if (mode_norms < 0).any(): print( 'Warning: mode norms has negative values. This is often happens ' 'when the rank of the vector matrix is much less than the number ' 'of columns. Try using fewer vectors (fewer columns).') # For sequential data, the user will provide a vecs # whose length is one larger than the number of columns of the # build_coeffs matrix. if vecs.shape[1] - build_coeffs.shape[0] == 1: modes = vec_space.lin_combine(vecs[:, :-1], build_coeffs, coeff_mat_col_indices=mode_indices) # For a non-sequential dataset, user provides vecs # whose length is equal to the number of columns of build_coeffs elif vecs.shape[1] == build_coeffs.shape[0]: modes = vec_space.lin_combine(vecs, build_coeffs, coeff_mat_col_indices=mode_indices) # Raise an error if number of handles isn't one of the two cases above. else: raise ValueError(('Number of cols in vecs does not match ' 'number of rows in build_coeffs matrix.')) if return_all: return modes, ritz_vals, mode_norms, build_coeffs else: return modes, ritz_vals, mode_norms
def compute_POD_matrices_direct_method(vecs, mode_indices, inner_product_weights=None, return_all=False): """Computes POD modes with data in a matrix using the direct method. Args: ``vecs``: Matrix of vectors stacked as columns. ``mode_indices``: List of mode indices to compute. Examples are ``range(10)`` or ``[3, 0, 6, 8]``. Kwargs: ``inner_product_weights``: 1D array or matrix of inner product weights. It corresponds to :math:`W` in inner product :math:`v_1^* W v_2`. ``return_all``: Return more objects, see below. Default is false. Returns: ``modes``: Matrix with requested modes as columns. ``eigen_vals``: 1D array of eigenvalues. These are the eigenvalues of the correlation matrix (:math:`X^* W X`), and are also the squares of the singular values of :math:`X`. If ``return_all`` is true, also returns: ``eigen_vecs``: Matrix of eigenvectors. These are the eigenvectors of correlation matrix (:math:`X^* W X`), and are also the right singular vectors of :math:`X`. The algorithm is 1. SVD :math:`U E V^* = W^{1/2} X` 2. Modes are :math:`W^{-1/2} U` where :math:`X`, :math:`W`, :math:`E`, :math:`V`, correspond to ``vecs``, ``inner_product_weights``, ``eigen_vals**0.5``, and ``eigen_vecs``, respectively. Since this method does not square the vectors and singular values, it is more accurate than taking the eigen decomposition of :math:`X^* W X`, as in the method of snapshots (:py:func:`compute_POD_arrays_direct_method`). However, this method is slower when :math:`X` has more rows than columns, i.e. there are fewer vectors than elements in each vector. """ if _parallel.is_distributed(): raise RuntimeError('Cannot run in parallel.') vecs = util.make_mat(vecs) if inner_product_weights is None: modes, sing_vals, eigen_vecs = util.svd(vecs) modes = modes[:, mode_indices] elif inner_product_weights.ndim == 1: sqrt_weights = inner_product_weights**0.5 vecs_weighted = N.mat(N.diag(sqrt_weights)) * vecs modes_weighted, sing_vals, eigen_vecs = util.svd(vecs_weighted) modes = N.mat(N.diag(sqrt_weights** -1.0)) * modes_weighted[:, mode_indices] elif inner_product_weights.ndim == 2: if inner_product_weights.shape[0] > 500: print 'Warning: Cholesky decomposition could be time consuming.' sqrt_weights = N.linalg.cholesky(inner_product_weights).H vecs_weighted = sqrt_weights * vecs modes_weighted, sing_vals, eigen_vecs = util.svd(vecs_weighted) modes = N.linalg.solve(sqrt_weights, modes_weighted[:, mode_indices]) #inv_sqrt_weights = N.linalg.inv(sqrt_weights) #modes = inv_sqrt_weights.dot(modes_weighted[:, mode_indices]) eigen_vals = sing_vals**2 if return_all: return modes, eigen_vals, eigen_vecs else: return modes, eigen_vals
def compute_BPOD_matrices(direct_vecs, adjoint_vecs, direct_mode_indices, adjoint_mode_indices, inner_product_weights=None, return_all=False): """Computes BPOD modes with data in a matrix. Args: ``direct_vecs``: Matrix with direct vecs as columns (:math:`X`). ``adjoint_vecs``: Matrix with adjoint vecs as columns (:math:`Y`). ``direct_mode_indices``: List of direct mode indices to compute. Examples are ``range(10)`` or ``[3, 0, 6, 8]``. ``adjoint_mode_indices``: List of adjoint mode indices to compute. Examples are ``range(10)`` or ``[3, 0, 6, 8]``. Kwargs: ``inner_product_weights``: 1D array or matrix of inner product weights. It corresponds to :math:`W` in inner product :math:`v_1^* W v_2`. ``return_all``: Return more objects, see below. Default is false. Returns: ``direct_modes``: Matrix with direct modes as columns. ``adjoint_modes``: Matrix with adjoint modes as columns. ``sing_vals``: 1D array of singular values of Hankel mat (:math:`E`). If ``return_all`` is true, then also returns: ``L_sing_vecs``: Matrix of left singular vectors of Hankel mat (:math:`U`). ``R_sing_vecs``: Matrix of right singular vectors of Hankel mat (:math:`V`). ``Hankel_mat``: Hankel matrix (:math:`Y^* W X`). See also :py:class:`BPODHandles`. """ if _parallel.is_distributed(): raise RuntimeError('Cannot run in parallel.') vec_space = VectorSpaceMatrices(weights=inner_product_weights) direct_vecs = util.make_mat(direct_vecs) adjoint_vecs = util.make_mat(adjoint_vecs) #Hankel_mat = vec_space.compute_inner_product_mat(adjoint_vecs, # direct_vecs) first_adjoint_all_direct = vec_space.compute_inner_product_mat(adjoint_vecs[:,0], direct_vecs) all_adjoint_last_direct = vec_space.compute_inner_product_mat(adjoint_vecs, direct_vecs[:,-1]) Hankel_mat = util.Hankel(first_adjoint_all_direct, all_adjoint_last_direct) L_sing_vecs, sing_vals, R_sing_vecs = util.svd(Hankel_mat) #print 'diff in Hankels',Hankel_mat - Hankel_mat2 #Hankel_mat = Hankel_mat2 sing_vals_sqrt_mat = N.mat(N.diag(sing_vals**-0.5)) direct_build_coeff_mat = R_sing_vecs * sing_vals_sqrt_mat direct_mode_array = vec_space.lin_combine(direct_vecs, direct_build_coeff_mat, coeff_mat_col_indices=direct_mode_indices) adjoint_build_coeff_mat = L_sing_vecs * sing_vals_sqrt_mat adjoint_mode_array = vec_space.lin_combine(adjoint_vecs, adjoint_build_coeff_mat, coeff_mat_col_indices=adjoint_mode_indices) if return_all: return direct_mode_array, adjoint_mode_array, sing_vals, L_sing_vecs, \ R_sing_vecs, Hankel_mat else: return direct_mode_array, adjoint_mode_array, sing_vals
def compute_BPOD_matrices(direct_vecs, adjoint_vecs, direct_mode_indices, adjoint_mode_indices, inner_product_weights=None, return_all=False): """Computes BPOD modes with data in a matrix. Args: ``direct_vecs``: Matrix with direct vecs as columns (:math:`X`). ``adjoint_vecs``: Matrix with adjoint vecs as columns (:math:`Y`). ``direct_mode_indices``: List of direct mode indices to compute. Examples are ``range(10)`` or ``[3, 0, 6, 8]``. ``adjoint_mode_indices``: List of adjoint mode indices to compute. Examples are ``range(10)`` or ``[3, 0, 6, 8]``. Kwargs: ``inner_product_weights``: 1D array or matrix of inner product weights. It corresponds to :math:`W` in inner product :math:`v_1^* W v_2`. ``return_all``: Return more objects, see below. Default is false. Returns: ``direct_modes``: Matrix with direct modes as columns. ``adjoint_modes``: Matrix with adjoint modes as columns. ``sing_vals``: 1D array of singular values of Hankel mat (:math:`E`). If ``return_all`` is true, then also returns: ``L_sing_vecs``: Matrix of left singular vectors of Hankel mat (:math:`U`). ``R_sing_vecs``: Matrix of right singular vectors of Hankel mat (:math:`V`). ``Hankel_mat``: Hankel matrix (:math:`Y^* W X`). See also :py:class:`BPODHandles`. """ if _parallel.is_distributed(): raise RuntimeError('Cannot run in parallel.') vec_space = VectorSpaceMatrices(weights=inner_product_weights) direct_vecs = util.make_mat(direct_vecs) adjoint_vecs = util.make_mat(adjoint_vecs) #Hankel_mat = vec_space.compute_inner_product_mat(adjoint_vecs, # direct_vecs) first_adjoint_all_direct = vec_space.compute_inner_product_mat( adjoint_vecs[:, 0], direct_vecs) all_adjoint_last_direct = vec_space.compute_inner_product_mat( adjoint_vecs, direct_vecs[:, -1]) Hankel_mat = util.Hankel(first_adjoint_all_direct, all_adjoint_last_direct) L_sing_vecs, sing_vals, R_sing_vecs = util.svd(Hankel_mat) #print 'diff in Hankels',Hankel_mat - Hankel_mat2 #Hankel_mat = Hankel_mat2 sing_vals_sqrt_mat = N.mat(N.diag(sing_vals**-0.5)) direct_build_coeff_mat = R_sing_vecs * sing_vals_sqrt_mat direct_mode_array = vec_space.lin_combine( direct_vecs, direct_build_coeff_mat, coeff_mat_col_indices=direct_mode_indices) adjoint_build_coeff_mat = L_sing_vecs * sing_vals_sqrt_mat adjoint_mode_array = vec_space.lin_combine( adjoint_vecs, adjoint_build_coeff_mat, coeff_mat_col_indices=adjoint_mode_indices) if return_all: return direct_mode_array, adjoint_mode_array, sing_vals, L_sing_vecs, \ R_sing_vecs, Hankel_mat else: return direct_mode_array, adjoint_mode_array, sing_vals
def compute_DMD_matrices_direct_method(vecs, mode_indices, adv_vecs=None, inner_product_weights=None, return_all=False): """Dynamic Mode Decomposition/Koopman Mode Decomposition with data in a matrix, using a direct method. Args: ``vecs``: Matrix with vectors as columns. ``mode_indices``: List of mode numbers, ``range(10)`` or ``[3, 0, 5]``. Kwargs: ``adv_vecs``: Matrix with ``vecs`` advanced in time as columns. If not provided, then it is assumed that the vectors are a sequential time-series. Thus ``vecs`` becomes ``vecs[:-1]`` and ``adv_vecs`` becomes ``vecs[1:]``. ``inner_product_weights``: 1D or Matrix of inner product weights. It corresponds to :math:`W` in inner product :math:`v_1^* W v_2`. ``return_all``: Return more objects, see below. Default is false. Returns: ``modes``: Matrix with requested modes as columns. ``ritz_vals``: 1D array of Ritz values. ``mode_norms``: 1D array of mode norms. If ``return_all`` is true, also returns: ``build_coeffs``: Matrix of build coefficients for modes. This method does not square the matrix of vectors as in the method of snapshots (:py:func:`compute_DMD_matrices_snaps_method`). It's slightly more accurate, but slower when the number of elements in a vector is more than the number of vectors (more rows than columns in ``vecs``). """ if _parallel.is_distributed(): raise RuntimeError('Cannot run in parallel.') vec_space = VectorSpaceMatrices(weights=inner_product_weights) vecs = util.make_mat(vecs) if adv_vecs is not None: adv_vecs = util.make_mat(adv_vecs) if inner_product_weights is None: vecs_weighted = vecs if adv_vecs is not None: adv_vecs_weighted = adv_vecs elif inner_product_weights.ndim == 1: sqrt_weights = N.mat(N.diag(inner_product_weights**0.5)) vecs_weighted = sqrt_weights * vecs if adv_vecs is not None: adv_vecs_weighted = sqrt_weights * adv_vecs elif inner_product_weights.ndim == 2: if inner_product_weights.shape[0] > 500: print 'Warning: Cholesky decomposition could be time consuming.' sqrt_weights = N.mat(N.linalg.cholesky(inner_product_weights)).H vecs_weighted = sqrt_weights * vecs if adv_vecs is not None: adv_vecs_weighted = sqrt_weights * adv_vecs # Compute low-order linear map for sequential snapshot set. This takes # advantage of the fact that for a sequential dataset, the unadvanced # and advanced vectors overlap. if adv_vecs is None: U, sing_vals, correlation_mat_evecs = util.svd(vecs_weighted[:,:-1]) correlation_mat_evals = sing_vals**2 correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H last_col = U.H * vecs_weighted[:,-1] correlation_mat_evals_sqrt = N.mat(N.diag(sing_vals**-1.0)) correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H low_order_linear_map = N.mat(N.concatenate( (correlation_mat_evals_sqrt * correlation_mat_evecs.H * \ correlation_mat[:, 1:], last_col), axis=1)) * \ correlation_mat_evecs * correlation_mat_evals_sqrt else: if vecs.shape != adv_vecs.shape: raise ValueError(('vecs and adv_vecs are not the same shape.')) U, sing_vals, correlation_mat_evecs = util.svd(vecs_weighted) correlation_mat_evals_sqrt = N.mat(N.diag(sing_vals**-1.0)) low_order_linear_map = U.H * adv_vecs_weighted * \ correlation_mat_evecs * correlation_mat_evals_sqrt correlation_mat_evals = sing_vals**2 correlation_mat = correlation_mat_evecs * \ N.mat(N.diag(correlation_mat_evals)) * correlation_mat_evecs.H # Compute eigendecomposition of low-order linear map. ritz_vals, low_order_evecs = N.linalg.eig(low_order_linear_map) build_coeffs = correlation_mat_evecs *\ correlation_mat_evals_sqrt * low_order_evecs *\ N.diag(N.array(N.array(N.linalg.inv( low_order_evecs.H * low_order_evecs) * low_order_evecs.H *\ correlation_mat_evals_sqrt * correlation_mat_evecs.H * correlation_mat[:, 0]).squeeze(), ndmin=1)) mode_norms = N.diag(build_coeffs.H * correlation_mat * build_coeffs).real if (mode_norms < 0).any(): print ('Warning: mode norms has negative values. This is often happens ' 'when the rank of the vector matrix is much less than the number ' 'of columns. Try using fewer vectors (fewer columns).') # For sequential data, the user will provide a vecs # whose length is one larger than the number of columns of the # build_coeffs matrix. if vecs.shape[1] - build_coeffs.shape[0] == 1: modes = vec_space.lin_combine(vecs[:, :-1], build_coeffs, coeff_mat_col_indices=mode_indices) # For a non-sequential dataset, user provides vecs # whose length is equal to the number of columns of build_coeffs elif vecs.shape[1] == build_coeffs.shape[0]: modes = vec_space.lin_combine(vecs, build_coeffs, coeff_mat_col_indices=mode_indices) # Raise an error if number of handles isn't one of the two cases above. else: raise ValueError(('Number of cols in vecs does not match ' 'number of rows in build_coeffs matrix.')) if return_all: return modes, ritz_vals, mode_norms, build_coeffs else: return modes, ritz_vals, mode_norms