From e13a26b4e2293fc28a407865abfebfba13cc70a6 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 13 Jun 2025 08:13:42 +0200 Subject: [PATCH 01/57] implementation --- src/sage/matrix/matrix2.pyx | 343 ++++++++++++++++++++ src/sage/matrix/matrix_polynomial_dense.pyx | 41 +++ 2 files changed, 384 insertions(+) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index a5cfab6aa02..15cdcbf7b90 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -18939,6 +18939,347 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U + def polynomial_compression(self,degree,variable): + """ + Returns the corresponding polynomial matrix where the coefficient matrix of degree i is the ith block of ``self``. + + INPUT: + + - ``degree`` -- integer: the number of blocks in the expanded matrix. If ``self`` does not have zero-blocks, + this corresponds to the degree of the resulting matrix. + - ``variable`` -- a symbolic variable (e.g., ``x``) to use in the polynomial. + + OUTPUT: + + - a polynomial matrix with the same number of rows and self.ncols()//(degree+1) columns + + EXAMPLES: + + The first 3 x 3 block of N corresponds to the + + sage: R. = GF(97)[] + sage: S. = ZZ[] + sage: M = matrix([[x^2+36*x,31*x,0],[3*x+13,x+57,0],[96,96,1]]) + sage: M + [x^2 + 36*x 31*x 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + sage: N = M.expansion() + sage: N + [ 0 0 0 36 31 0 1 0 0] + [13 57 0 3 1 0 0 0 0] + [96 96 1 0 0 0 0 0 0] + sage: N.polynomial_compression(2,x) + [x^2 + 36*x 31*x 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + sage: N.polynomial_compression(2,y) + [y^2 + 36*y 31*y 0] + [ 3*y + 13 y + 57 0] + [ 96 96 1] + """ + if self.ncols() % (degree+1) != 0: + raise ValueError('The column number must be divisible by degree+1.') + coeff_matrices = [self[:,i*(self.ncols()//(degree+1)):(i+1)*(self.ncols()//(degree+1))] for i in range(degree+1)] + return sum([coeff_matrices[i]*variable**i for i in range(degree+1)]) + + def striped_krylov_matrix(self, J, degree, shift=None): + r""" + Return the block matrix of the following form, with rows permuted according to shift. + The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. + + [ ] + [ E ] + [ ] + [-----] + [ ] + [ EJ ] + [ ] + [-----] + [ . ] + [ . ] + [ . ] + [-----] + [ ] + [ EJ^d] + [ ] + + INPUT: + + - ``J`` -- a square matrix of size equal to the number of columns of ``self``. + - ``degree`` -- integer, the maximum exponent for the Krylov matrix. + - ``shift`` -- list of integers (optional), row priority shifts. If ``None``, + defaults to all zero. + + OUTPUT: + + - A matrix with block rows [E, EJ, EJ^2, ..., EJ^d], row-permuted by shift. + + EXAMPLES: + + sage: R. = GF(97)[] + sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(J,3) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.striped_krylov_matrix(J,3,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.striped_krylov_matrix(J,3,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + + """ + from sage.combinat.permutation import Permutation + from sage.matrix.constructor import matrix # for matrix.block + + m = self.nrows() + + # if no shift, this is equivalent to 0s + if shift is None: + shift = [0]*self.nrows() + + # size checks + if len(shift) != m: + raise ValueError(f"The shift should have the same number of elements as the rows of the matrix ({m}), but had {len(shift)}.") + if not J.is_square() or J.nrows() != self.ncols(): + raise ValueError(f"The matrix J should be a square matrix and match the number of columns of self ({self.ncols()}), but is of dimension {J.nrows()} x {J.ncols()}.") + + # define priority and indexing functions + # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + + # deduce permutation by sorting priorities + # rows should be ordered by ascending priority, then by associated row number in E (i%m) + priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + + # build all blocks for the striped matrix + blocks = [] + + # for i from 0 to degree (inclusive), add E*J^i + J_i = matrix.identity(self.ncols()) + for i in range(degree+1): + blocks.append([self*J_i]) + if i < degree: + J_i *= J + + # return block matrix permuted according to shift + krylov = matrix.block(blocks,subdivide=False) + krylov.permute_rows(priority_permutation) + return krylov + + def naive_krylov_rank_profile(self, J, degree, shift=None): + r""" + Compute the rank profile (row and column) of the striped Krylov matrix + built from ``self`` and matrix `J`. + + INPUT: + + - ``J`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of J in Krylov matrix. + - ``shift`` -- list of integers (optional): priority row shift. + + OUTPUT: + + - A tuple (row_profile, pivot_matrix, column_profile): + * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows + * pivot: the submatrix of `K` given by those rows + * column_profile: list of the first r independent column indices in ``pivot_matrix`` + where r is the rank of `K`. + + EXAMPLES: + + sage: R. = GF(97)[] + sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(J,3) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3,[0,3,6]) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3,[3,0,2]) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + """ + from sage.matrix.constructor import matrix + + K = self.striped_krylov_matrix(J,degree,shift) + + # calculate first independent rows of K + row_profile = K.transpose().pivots() + + # construct submatrix + pivot = K.matrix_from_rows(row_profile) + + # calculate first independent columns of pivot + col_profile = pivot.pivots() + + return row_profile,pivot,col_profile + + def linear_interpolation_basis(self, J, degree, shift=None): + r""" + Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. + + INPUT: + + - ``J`` -- square matrix for Krylov construction. + - ``degree`` -- upper bound on degree of minpoly(`J`), power of + - ``shift`` -- list of integers (optional): controls priority of rows. + + OUTPUT: + + - Matrix whose rows form an interpolation basis. + + """ + from sage.combinat.permutation import Permutation + from sage.matrix.constructor import matrix + + m = self.nrows() + + # if no shift, this is equivalent to 0s + if shift is None: + shift = [0]*self.nrows() + + # calculate shift priority function and permutation + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + index_inv = lambda c,d : c + d*m + + priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + + # maps row c of EJ^d to position i in striped Krylov matrix + # +/- 1 as permutations are 1-indexed, matrices are 0-indexed + phi = lambda c,d : priority_permutation.inverse()(index_inv(c,d) + 1) - 1 + phi_inv = lambda i : index(priority_permutation(i + 1) - 1) + + # calculate krylov profile + row_profile, pivot, col_profile = self.naive_krylov_rank_profile(J,degree,shift) + + # (c_k, d_k) = phi^-1 (row_i) + c, d = zip(*(phi_inv(i) for i in row_profile)) + + # degree_c = 0 or max d[k] such that c[k] = c + degree_c = [0]*m + for k in range(len(row_profile)): + if d[k] >= degree_c[c[k]]: + degree_c[c[k]] = d[k] + 1 + + # compute striped Krylov matrix + K = self.striped_krylov_matrix(J,degree,shift) + + # compute submatrix of target with rows phi(i,degree_c[i]) + target = K.matrix_from_rows([phi(i,degree_c[i]) for i in range(m)]) + + # compute submatrices of pivot and target with col_profile + C = pivot.matrix_from_columns(col_profile) + D = target.matrix_from_columns(col_profile) + + relation = D*C.inverse() + + # linear interpolation basis in shifted Popov form + uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) + + return uncompressed_basis + # a limited number of access-only properties are provided for matrices @property def T(self): @@ -19562,6 +19903,8 @@ def _matrix_power_symbolic(A, n): return P * M * Pinv + + class NotFullRankError(ValueError): """ diff --git a/src/sage/matrix/matrix_polynomial_dense.pyx b/src/sage/matrix/matrix_polynomial_dense.pyx index db6ff8f4ccf..4e147c2f82e 100644 --- a/src/sage/matrix/matrix_polynomial_dense.pyx +++ b/src/sage/matrix/matrix_polynomial_dense.pyx @@ -5337,3 +5337,44 @@ cdef class Matrix_polynomial_dense(Matrix_generic_dense): return False return True + + def expansion(self, degree=None): + r""" + Return the horizontal expansion of the matrix, + where the ith block corresponds to the coefficients for x^i. + + INPUT: + + - ``degree`` -- integer, the degree bound on the matrix; + if ``None``, defaults to degree of matrix. + + OUTPUT: matrix + + EXAMPLES: + sage: R. = GF(97)[] + sage: M = matrix([[x^2+36*x,31*x,0],[3*x+13,x+57,0],[96,96,1]]) + sage: M + [x^2 + 36*x 31*x 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + sage: M.expansion() + [ 0 0 0 36 31 0 1 0 0] + [13 57 0 3 1 0 0 0 0] + [96 96 1 0 0 0 0 0 0] + sage: M.expansion(1) + [ 0 0 0 36 31 0] + [13 57 0 3 1 0] + [96 96 1 0 0 0] + sage: M.expansion(3) + [ 0 0 0 36 31 0 1 0 0 0 0 0] + [13 57 0 3 1 0 0 0 0 0 0 0] + [96 96 1 0 0 0 0 0 0 0 0 0] + """ + from sage.matrix.constructor import matrix # for matrix.block + + if degree is None: + degree = self.degree() + + # compute each coefficient matrix + coeff_matrices = [[self.coefficient_matrix(i) for i in range(degree+1)]] + return matrix.block(coeff_matrices,subdivide=False) \ No newline at end of file From f69c4026e21ec3c670c4f9abc4657ba78aff7d9e Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 16 Jun 2025 13:03:35 +0200 Subject: [PATCH 02/57] compression for linear interpolation basis --- src/sage/matrix/matrix2.pyx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 15cdcbf7b90..e3289fb0e1c 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19214,7 +19214,7 @@ cdef class Matrix(Matrix1): return row_profile,pivot,col_profile - def linear_interpolation_basis(self, J, degree, shift=None): + def linear_interpolation_basis(self, J, degree, variable, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19278,7 +19278,14 @@ cdef class Matrix(Matrix1): # linear interpolation basis in shifted Popov form uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) - return uncompressed_basis + basis_rows = [[0]*m for i in range(m)] + for col in range(relation.ncols()): + for row in range(m): + basis_rows[row][c[col]] += uncompressed_basis[row][col] * variable**d[col] + for i in range(m): + basis_rows[i][i] += variable**degree_c[i] + + return matrix(basis_rows) # a limited number of access-only properties are provided for matrices @property From a415dccedfba2cab506e9a5f548e11cfa266a9a1 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 16 Jun 2025 17:11:53 +0200 Subject: [PATCH 03/57] fast krylov almost working, problem with indices being too large for lambda --- src/sage/matrix/matrix2.pyx | 139 ++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index e3289fb0e1c..a7a88491811 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19214,6 +19214,144 @@ cdef class Matrix(Matrix1): return row_profile,pivot,col_profile + def krylov_rank_profile(self, J, degree, shift=None): + r""" + Compute the rank profile (row and column) of the striped Krylov matrix + built from ``self`` and matrix `J`. + + INPUT: + + - ``J`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of J in Krylov matrix. + - ``shift`` -- list of integers (optional): priority row shift. + + OUTPUT: + + - A tuple (row_profile, pivot_matrix, column_profile): + * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows + * pivot: the submatrix of `K` given by those rows + * column_profile: list of the first r independent column indices in ``pivot_matrix`` + where r is the rank of `K`. + + EXAMPLES: + + sage: R. = GF(97)[] + sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(J,3) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3,[0,3,6]) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.naive_krylov_rank_profile(J,3,[3,0,2]) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + """ + from sage.matrix.constructor import matrix + import math + from sage.combinat.permutation import Permutation + + m = self.nrows() + + # calculate shift priority function and permutation + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + index_inv = lambda c,d : c + d*m + + priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + + # maps row c of EJ^d to position i in striped Krylov matrix + # +/- 1 as permutations are 1-indexed, matrices are 0-indexed + phi = lambda c,d : priority_permutation.inverse()(index_inv(c,d) + 1) - 1 + phi_inv = lambda i : index(priority_permutation(i + 1) - 1) + + row_profile_self = self.transpose().pivots() + r = len(row_profile_self) + row_profile_K = sorted([phi(row,0) for row in row_profile_self]) + + M = matrix([self.row(index_inv(*phi_inv(i))) for i in row_profile_K]) + + J_L = None + for l in range(math.ceil(math.log(degree,2)) + 1): + c,d = zip(*(phi_inv(i) for i in row_profile_K)) + L = pow(2,l) + row_extension = sorted([phi(c[i],d[i] + L) for i in range(r)]) + + k = sorted(row_profile_K + row_extension) + + if J_L is None: + J_L = J + else: + J_L = J_L * J_L + + M = matrix.block([[M],[M*J_L]],subdivide=False) + + row_profile_M = M.transpose().pivots() + M = matrix([M.row(i) for i in row_profile_M]) + + row_profile_K = [k[i] for i in row_profile_M] + r = len(row_profile_K) + + print(c,d,L,row_extension,k) + def linear_interpolation_basis(self, J, degree, variable, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19278,6 +19416,7 @@ cdef class Matrix(Matrix1): # linear interpolation basis in shifted Popov form uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) + # compression of basis into polynomial form basis_rows = [[0]*m for i in range(m)] for col in range(relation.ncols()): for row in range(m): From 59663bcb43a24ccafe24e1952ef9a222bd11fc1c Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 18 Jun 2025 12:54:51 +0200 Subject: [PATCH 04/57] krylov_rank_profile working now --- src/sage/matrix/matrix2.pyx | 41 ++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index a7a88491811..1291dbe3cfb 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19259,7 +19259,7 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3) + sage: E.krylov_rank_profile(J,3) ( [27 49 29] [50 58 0] @@ -19278,7 +19278,7 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3,[0,3,6]) + sage: E.krylov_rank_profile(J,3,[0,3,6]) ( [27 49 29] [ 0 27 49] @@ -19297,7 +19297,7 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3,[3,0,2]) + sage: E.krylov_rank_profile(J,3,[3,0,2]) ( [50 58 0] [ 0 50 58] @@ -19318,39 +19318,52 @@ cdef class Matrix(Matrix1): priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - # maps row c of EJ^d to position i in striped Krylov matrix + # maps row c of self*J^d to position i in striped Krylov matrix # +/- 1 as permutations are 1-indexed, matrices are 0-indexed phi = lambda c,d : priority_permutation.inverse()(index_inv(c,d) + 1) - 1 phi_inv = lambda i : index(priority_permutation(i + 1) - 1) - row_profile_self = self.transpose().pivots() - r = len(row_profile_self) - row_profile_K = sorted([phi(row,0) for row in row_profile_self]) + # calculate row profile of self, with shift applied + self_permutation = Permutation([pair[1]+1 for pair in sorted([[phi(i,0),i] for i in range(m)])]) + row_profile_self_permuted = self.with_permuted_rows(self_permutation).transpose().pivots() + row_profile_self = [self_permutation(i+1)-1 for i in row_profile_self_permuted] + + # base row_profile_K using row_profile_self + row_profile_K = sorted([phi(i,0) for i in row_profile_self]) + r = len(row_profile_K) M = matrix([self.row(index_inv(*phi_inv(i))) for i in row_profile_K]) J_L = None for l in range(math.ceil(math.log(degree,2)) + 1): + # row and degree of profile within unshifted expansion c,d = zip(*(phi_inv(i) for i in row_profile_K)) L = pow(2,l) - row_extension = sorted([phi(c[i],d[i] + L) for i in range(r)]) + # adding 2^l to each degree + row_extension = sorted([phi(c[i],d[i] + L) for i in range(r) if d[i] + L <= degree]) - k = sorted(row_profile_K + row_extension) + # concatenate two sequences (order preserved) + k = row_profile_K + row_extension + # calculate sorting permutation + k_perm = Permutation([x[1] for x in sorted([k[i],i+1] for i in range(len(k)))]) + # fast calculation of rows formed by indices in k if J_L is None: J_L = J else: J_L = J_L * J_L - M = matrix.block([[M],[M*J_L]],subdivide=False) + # sort rows of M, find profile, translate to k (indices of full krylov matrix) + M.permute_rows(k_perm) row_profile_M = M.transpose().pivots() - M = matrix([M.row(i) for i in row_profile_M]) - - row_profile_K = [k[i] for i in row_profile_M] r = len(row_profile_K) + row_profile_K = [k[k_perm(i+1)-1] for i in row_profile_M] - print(c,d,L,row_extension,k) + # calculate new M for return value or next loop + M = matrix([M.row(i) for i in row_profile_M]) + col_profile = M.pivots() + return tuple(row_profile_K), M, col_profile def linear_interpolation_basis(self, J, degree, variable, shift=None): r""" From 21882b5973a621c84d8a03fcbff0cf29c6de3786 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 18 Jun 2025 14:28:52 +0200 Subject: [PATCH 05/57] optimised target calculation --- src/sage/matrix/matrix2.pyx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 1291dbe3cfb..aefc35e7da7 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19409,16 +19409,37 @@ cdef class Matrix(Matrix1): c, d = zip(*(phi_inv(i) for i in row_profile)) # degree_c = 0 or max d[k] such that c[k] = c + inv_c = [None]*m degree_c = [0]*m for k in range(len(row_profile)): if d[k] >= degree_c[c[k]]: degree_c[c[k]] = d[k] + 1 + inv_c[c[k]] = k + ################# + # OPTIMISATION + ################# + T_present = matrix([pivot.row(inv_c[i]) for i in range(m) if inv_c[i] is not None]) * J + T_absent = matrix([self.row(i) for i in range(m) if inv_c[i] is None]) + T_rows = [] + idx_p = 0 + idx_a = 0 + for i in range(m): + if inv_c[i] is None: + T_rows.append(T_absent[idx_a]) + idx_a += 1 + else: + T_rows.append(T_present[idx_p]) + idx_p += 1 + target = matrix(T_rows) + ################# + # ELSE # compute striped Krylov matrix - K = self.striped_krylov_matrix(J,degree,shift) + #K = self.striped_krylov_matrix(J,degree,shift) # compute submatrix of target with rows phi(i,degree_c[i]) - target = K.matrix_from_rows([phi(i,degree_c[i]) for i in range(m)]) + #target = K.matrix_from_rows([phi(i,degree_c[i]) for i in range(m)]) + ################# # compute submatrices of pivot and target with col_profile C = pivot.matrix_from_columns(col_profile) From ce717fedbee83298727650d0aa35e0b3be51812c Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 19 Jun 2025 03:39:43 +0200 Subject: [PATCH 06/57] combine row and column submatrix operations in basis calc --- src/sage/matrix/matrix2.pyx | 39 +++++++++++++------------------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index aefc35e7da7..e5e4902bdfc 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19365,7 +19365,7 @@ cdef class Matrix(Matrix1): col_profile = M.pivots() return tuple(row_profile_K), M, col_profile - def linear_interpolation_basis(self, J, degree, variable, shift=None): + def linear_interpolation_basis(self, J, degree, variable, shift=None, opt=False): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19416,34 +19416,23 @@ cdef class Matrix(Matrix1): degree_c[c[k]] = d[k] + 1 inv_c[c[k]] = k - ################# - # OPTIMISATION - ################# - T_present = matrix([pivot.row(inv_c[i]) for i in range(m) if inv_c[i] is not None]) * J - T_absent = matrix([self.row(i) for i in range(m) if inv_c[i] is None]) - T_rows = [] + T_absent_indices = [i for i in range(m) if inv_c[i] is None] + T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] + D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) + D_present = (pivot.matrix_from_rows(T_present_indices)*J).matrix_from_columns(col_profile) + D_rows = [] idx_p = 0 idx_a = 0 for i in range(m): - if inv_c[i] is None: - T_rows.append(T_absent[idx_a]) - idx_a += 1 - else: - T_rows.append(T_present[idx_p]) - idx_p += 1 - target = matrix(T_rows) - ################# - # ELSE - # compute striped Krylov matrix - #K = self.striped_krylov_matrix(J,degree,shift) - - # compute submatrix of target with rows phi(i,degree_c[i]) - #target = K.matrix_from_rows([phi(i,degree_c[i]) for i in range(m)]) - ################# - - # compute submatrices of pivot and target with col_profile + if inv_c[i] is None: + D_rows.append(D_absent[idx_a]) + idx_a += 1 + else: + D_rows.append(D_present[idx_p]) + idx_p += 1 + C = pivot.matrix_from_columns(col_profile) - D = target.matrix_from_columns(col_profile) + D = matrix(D_rows) relation = D*C.inverse() From c4ae3ea14abcd9347c65f67c26cc17844f3655cf Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 19 Jun 2025 10:34:54 +0200 Subject: [PATCH 07/57] small bug fixes and cases with 0 rows --- src/sage/matrix/matrix2.pyx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index e5e4902bdfc..4edc907ed38 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19310,6 +19310,9 @@ cdef class Matrix(Matrix1): m = self.nrows() + if m == 0: + return (),self,() + # calculate shift priority function and permutation priority = lambda c,d : shift[c] + d index = lambda i : (i%m,i//m) @@ -19332,7 +19335,10 @@ cdef class Matrix(Matrix1): row_profile_K = sorted([phi(i,0) for i in row_profile_self]) r = len(row_profile_K) - M = matrix([self.row(index_inv(*phi_inv(i))) for i in row_profile_K]) + if r == 0: + return (),self.matrix_from_rows([]),() + + M = self.matrix_from_rows([index_inv(*phi_inv(i)) for i in row_profile_K]) J_L = None for l in range(math.ceil(math.log(degree,2)) + 1): @@ -19357,7 +19363,7 @@ cdef class Matrix(Matrix1): # sort rows of M, find profile, translate to k (indices of full krylov matrix) M.permute_rows(k_perm) row_profile_M = M.transpose().pivots() - r = len(row_profile_K) + r = len(row_profile_M) row_profile_K = [k[k_perm(i+1)-1] for i in row_profile_M] # calculate new M for return value or next loop @@ -19365,7 +19371,7 @@ cdef class Matrix(Matrix1): col_profile = M.pivots() return tuple(row_profile_K), M, col_profile - def linear_interpolation_basis(self, J, degree, variable, shift=None, opt=False): + def linear_interpolation_basis(self, J, degree, variable, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19403,7 +19409,10 @@ cdef class Matrix(Matrix1): phi_inv = lambda i : index(priority_permutation(i + 1) - 1) # calculate krylov profile - row_profile, pivot, col_profile = self.naive_krylov_rank_profile(J,degree,shift) + row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift) + + if len(row_profile) == 0: + return matrix.identity(self.base_ring(),m) # (c_k, d_k) = phi^-1 (row_i) c, d = zip(*(phi_inv(i) for i in row_profile)) From 308333f230e4577a2e7c88389251c8aaee32b75e Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 19 Jun 2025 14:18:50 +0200 Subject: [PATCH 08/57] performance fix for lambda - calculate inverse once --- src/sage/matrix/matrix2.pyx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 4edc907ed38..de8bbd3f3e1 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19306,8 +19306,14 @@ cdef class Matrix(Matrix1): """ from sage.matrix.constructor import matrix import math + import time from sage.combinat.permutation import Permutation + #DEBUG + time_idx = 0 + start = time.time() + #DEBUG + m = self.nrows() if m == 0: @@ -19320,14 +19326,16 @@ cdef class Matrix(Matrix1): priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + priority_permutation_inv = priority_permutation.inverse() # maps row c of self*J^d to position i in striped Krylov matrix # +/- 1 as permutations are 1-indexed, matrices are 0-indexed - phi = lambda c,d : priority_permutation.inverse()(index_inv(c,d) + 1) - 1 + phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 phi_inv = lambda i : index(priority_permutation(i + 1) - 1) # calculate row profile of self, with shift applied self_permutation = Permutation([pair[1]+1 for pair in sorted([[phi(i,0),i] for i in range(m)])]) + row_profile_self_permuted = self.with_permuted_rows(self_permutation).transpose().pivots() row_profile_self = [self_permutation(i+1)-1 for i in row_profile_self_permuted] @@ -19402,10 +19410,11 @@ cdef class Matrix(Matrix1): priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + priority_permutation_inv = priority_permutation.inverse() # maps row c of EJ^d to position i in striped Krylov matrix # +/- 1 as permutations are 1-indexed, matrices are 0-indexed - phi = lambda c,d : priority_permutation.inverse()(index_inv(c,d) + 1) - 1 + phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 phi_inv = lambda i : index(priority_permutation(i + 1) - 1) # calculate krylov profile From 4126fec1cfaec559d100bc933ccce2c2764d69bf Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 20 Jun 2025 13:19:23 +0200 Subject: [PATCH 09/57] remove debug code, tidy up variable handling for polynomial ring --- src/sage/matrix/matrix2.pyx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index de8bbd3f3e1..2852e8084ab 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -18939,7 +18939,7 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def polynomial_compression(self,degree,variable): + def polynomial_compression(self,degree,var_name): """ Returns the corresponding polynomial matrix where the coefficient matrix of degree i is the ith block of ``self``. @@ -18978,6 +18978,11 @@ cdef class Matrix(Matrix1): [ 3*y + 13 y + 57 0] [ 96 96 1] """ + from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing + + poly_ring = PolynomialRing(self.base_ring(),var_name) + variable = poly_ring.gen() + if self.ncols() % (degree+1) != 0: raise ValueError('The column number must be divisible by degree+1.') coeff_matrices = [self[:,i*(self.ncols()//(degree+1)):(i+1)*(self.ncols()//(degree+1))] for i in range(degree+1)] @@ -19306,14 +19311,8 @@ cdef class Matrix(Matrix1): """ from sage.matrix.constructor import matrix import math - import time from sage.combinat.permutation import Permutation - #DEBUG - time_idx = 0 - start = time.time() - #DEBUG - m = self.nrows() if m == 0: @@ -19379,7 +19378,7 @@ cdef class Matrix(Matrix1): col_profile = M.pivots() return tuple(row_profile_K), M, col_profile - def linear_interpolation_basis(self, J, degree, variable, shift=None): + def linear_interpolation_basis(self, J, degree, var_name, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19396,6 +19395,9 @@ cdef class Matrix(Matrix1): """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix + from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing + + poly_ring = PolynomialRing(self.base_ring(),var_name) m = self.nrows() @@ -19421,7 +19423,7 @@ cdef class Matrix(Matrix1): row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift) if len(row_profile) == 0: - return matrix.identity(self.base_ring(),m) + return matrix.identity(poly_ring,m) # (c_k, d_k) = phi^-1 (row_i) c, d = zip(*(phi_inv(i) for i in row_profile)) @@ -19457,6 +19459,9 @@ cdef class Matrix(Matrix1): # linear interpolation basis in shifted Popov form uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) + # construct variable + variable = poly_ring.gen() + # compression of basis into polynomial form basis_rows = [[0]*m for i in range(m)] for col in range(relation.ncols()): From 346aaab776aedf5bbd83fd9452ff774239ad4172 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Tue, 1 Jul 2025 21:06:47 +0200 Subject: [PATCH 10/57] add caching for row_pivots in calculation of col_pivots for matrix_modn_dense, add aliases for pivots and pivot_rows, optimisation for krylov_rank_profile by minimising index conversion and full permutation calculation --- src/sage/matrix/matrix2.pyx | 178 +++++++++++++++++- .../matrix/matrix_modn_dense_template.pxi | 8 +- 2 files changed, 177 insertions(+), 9 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 2852e8084ab..20ef0354464 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -1027,6 +1027,22 @@ cdef class Matrix(Matrix1): v = self.transpose().pivots() self.cache('pivot_rows', v) return v + + def row_rank_profile(self): + """ + Alias for pivot_rows(self). Return the pivot row positions for this matrix, which are a topmost + subset of the rows that span the row space and are linearly + independent. + """ + return self.pivot_rows() + + def col_rank_profile(self): + """ + Alias for pivots(self). Return the pivot column positions for this matrix, which are a leftmost + subset of the columns that span the column space and are linearly + independent. + """ + return self.pivots() def _solve_right_general(self, B, check=True): r""" @@ -19209,13 +19225,13 @@ cdef class Matrix(Matrix1): K = self.striped_krylov_matrix(J,degree,shift) # calculate first independent rows of K - row_profile = K.transpose().pivots() + row_profile = K.row_rank_profile() # construct submatrix pivot = K.matrix_from_rows(row_profile) # calculate first independent columns of pivot - col_profile = pivot.pivots() + col_profile = pivot.col_rank_profile() return row_profile,pivot,col_profile @@ -19335,7 +19351,7 @@ cdef class Matrix(Matrix1): # calculate row profile of self, with shift applied self_permutation = Permutation([pair[1]+1 for pair in sorted([[phi(i,0),i] for i in range(m)])]) - row_profile_self_permuted = self.with_permuted_rows(self_permutation).transpose().pivots() + row_profile_self_permuted = self.with_permuted_rows(self_permutation).row_rank_profile() row_profile_self = [self_permutation(i+1)-1 for i in row_profile_self_permuted] # base row_profile_K using row_profile_self @@ -19352,11 +19368,13 @@ cdef class Matrix(Matrix1): # row and degree of profile within unshifted expansion c,d = zip(*(phi_inv(i) for i in row_profile_K)) L = pow(2,l) + # adding 2^l to each degree row_extension = sorted([phi(c[i],d[i] + L) for i in range(r) if d[i] + L <= degree]) # concatenate two sequences (order preserved) k = row_profile_K + row_extension + # calculate sorting permutation k_perm = Permutation([x[1] for x in sorted([k[i],i+1] for i in range(len(k)))]) @@ -19365,17 +19383,165 @@ cdef class Matrix(Matrix1): J_L = J else: J_L = J_L * J_L - M = matrix.block([[M],[M*J_L]],subdivide=False) + M = M.stack(M*J_L,subdivide=False) # sort rows of M, find profile, translate to k (indices of full krylov matrix) M.permute_rows(k_perm) - row_profile_M = M.transpose().pivots() + row_profile_M = M.row_rank_profile() r = len(row_profile_M) row_profile_K = [k[k_perm(i+1)-1] for i in row_profile_M] # calculate new M for return value or next loop M = matrix([M.row(i) for i in row_profile_M]) - col_profile = M.pivots() + col_profile = M.col_rank_profile() + return tuple(row_profile_K), M, col_profile + + def krylov_rank_profile_fast_perm(self, J, degree, shift=None): + r""" + Compute the rank profile (row and column) of the striped Krylov matrix + built from ``self`` and matrix `J`. + + INPUT: + + - ``J`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of J in Krylov matrix. + - ``shift`` -- list of integers (optional): priority row shift. + + OUTPUT: + + - A tuple (row_profile, pivot_matrix, column_profile): + * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows + * pivot: the submatrix of `K` given by those rows + * column_profile: list of the first r independent column indices in ``pivot_matrix`` + where r is the rank of `K`. + + EXAMPLES: + + sage: R. = GF(97)[] + sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(J,3) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3,[0,3,6]) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3,[3,0,2]) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + """ + from sage.matrix.constructor import matrix + import math + from sage.combinat.permutation import Permutation + + m = self.nrows() + + if m == 0: + return (),self,() + + # calculate row profile of self, with shift applied + self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) + + self_permuted = self.with_permuted_rows(self_permutation) + row_profile_self_permuted = self_permuted.row_rank_profile() + row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] + + r = len(row_profile_self) + + if r == 0: + return (),self.matrix_from_rows([]),() + + M = self_permuted.matrix_from_rows(row_profile_self_permuted) + + J_L = None + + for l in range(math.ceil(math.log(degree,2)) + 1): + L = pow(2,l) + # adding 2^l to each degree + row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] + + # concatenate two sequences (order preserved) + k = row_profile_self + row_extension + + # calculate sorting permutation, sort k by (shift[c]+d, c) + k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + + # fast calculation of rows formed by indices in k + if J_L is None: + J_L = J + else: + J_L = J_L * J_L + M = M.stack(M*J_L,subdivide=False) + + # sort rows of M, find profile, translate to k (indices of full krylov matrix) + M.permute_rows(k_perm) + row_profile_M = M.row_rank_profile() + r = len(row_profile_M) + row_profile_self = sorted([k[k_perm(i+1)-1] for i in row_profile_M],key=lambda x: (shift[x[0]]+x[1],x[0])) + + # calculate new M for return value or next loop + M = matrix([M.row(i) for i in row_profile_M]) + # convert c,d to actual position in striped Krylov matrix + phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(len(shift))) + row_profile_K = [phi(*row) for row in row_profile_self] + + col_profile = M.col_rank_profile() return tuple(row_profile_K), M, col_profile def linear_interpolation_basis(self, J, degree, var_name, shift=None): diff --git a/src/sage/matrix/matrix_modn_dense_template.pxi b/src/sage/matrix/matrix_modn_dense_template.pxi index 8e9e7bf0f89..63fac0af4b0 100644 --- a/src/sage/matrix/matrix_modn_dense_template.pxi +++ b/src/sage/matrix/matrix_modn_dense_template.pxi @@ -204,12 +204,13 @@ cdef inline linbox_echelonize(celement modulus, celement* entries, Py_ssize_t nr applyP(F[0], FflasRight, FflasNoTrans, nrows, 0, r, entries, ncols, Q) - cdef list pivots = [int(Q[i]) for i in range(r)] + cdef list column_pivots = [int(Q[i]) for i in range(r)] + cdef list row_pivots = sorted([int(P[i]) for i in range(r)]) sig_free(P) sig_free(Q) del F - return r, pivots + return r, column_pivots, row_pivots cdef inline linbox_echelonize_efd(celement modulus, celement* entries, Py_ssize_t nrows, Py_ssize_t ncols): # See trac #13878: This is to avoid sending invalid data to linbox, @@ -1788,8 +1789,9 @@ cdef class Matrix_modn_dense_template(Matrix_dense): r, pivots = linbox_echelonize_efd(self.p, self._entries, self._nrows, self._ncols) else: - r, pivots = linbox_echelonize(self.p, self._entries, + r, pivots, row_pivots = linbox_echelonize(self.p, self._entries, self._nrows, self._ncols) + self.cache('pivot_rows',tuple(row_pivots)) verbose('done with echelonize', t) self.cache('in_echelon_form', True) self.cache('rank', r) From a41e6ec5d08b60428c3c332387885440fba64bfe Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 2 Jul 2025 14:01:47 +0200 Subject: [PATCH 11/57] make optimisation fast perm profile main implementation --- src/sage/matrix/matrix2.pyx | 161 ------------------------------------ 1 file changed, 161 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 20ef0354464..779cf473084 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19331,167 +19331,6 @@ cdef class Matrix(Matrix1): m = self.nrows() - if m == 0: - return (),self,() - - # calculate shift priority function and permutation - priority = lambda c,d : shift[c] + d - index = lambda i : (i%m,i//m) - index_inv = lambda c,d : c + d*m - - priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) - priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - priority_permutation_inv = priority_permutation.inverse() - - # maps row c of self*J^d to position i in striped Krylov matrix - # +/- 1 as permutations are 1-indexed, matrices are 0-indexed - phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 - phi_inv = lambda i : index(priority_permutation(i + 1) - 1) - - # calculate row profile of self, with shift applied - self_permutation = Permutation([pair[1]+1 for pair in sorted([[phi(i,0),i] for i in range(m)])]) - - row_profile_self_permuted = self.with_permuted_rows(self_permutation).row_rank_profile() - row_profile_self = [self_permutation(i+1)-1 for i in row_profile_self_permuted] - - # base row_profile_K using row_profile_self - row_profile_K = sorted([phi(i,0) for i in row_profile_self]) - r = len(row_profile_K) - - if r == 0: - return (),self.matrix_from_rows([]),() - - M = self.matrix_from_rows([index_inv(*phi_inv(i)) for i in row_profile_K]) - - J_L = None - for l in range(math.ceil(math.log(degree,2)) + 1): - # row and degree of profile within unshifted expansion - c,d = zip(*(phi_inv(i) for i in row_profile_K)) - L = pow(2,l) - - # adding 2^l to each degree - row_extension = sorted([phi(c[i],d[i] + L) for i in range(r) if d[i] + L <= degree]) - - # concatenate two sequences (order preserved) - k = row_profile_K + row_extension - - # calculate sorting permutation - k_perm = Permutation([x[1] for x in sorted([k[i],i+1] for i in range(len(k)))]) - - # fast calculation of rows formed by indices in k - if J_L is None: - J_L = J - else: - J_L = J_L * J_L - M = M.stack(M*J_L,subdivide=False) - - # sort rows of M, find profile, translate to k (indices of full krylov matrix) - M.permute_rows(k_perm) - row_profile_M = M.row_rank_profile() - r = len(row_profile_M) - row_profile_K = [k[k_perm(i+1)-1] for i in row_profile_M] - - # calculate new M for return value or next loop - M = matrix([M.row(i) for i in row_profile_M]) - col_profile = M.col_rank_profile() - return tuple(row_profile_K), M, col_profile - - def krylov_rank_profile_fast_perm(self, J, degree, shift=None): - r""" - Compute the rank profile (row and column) of the striped Krylov matrix - built from ``self`` and matrix `J`. - - INPUT: - - - ``J`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of J in Krylov matrix. - - ``shift`` -- list of integers (optional): priority row shift. - - OUTPUT: - - - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows - * pivot: the submatrix of `K` given by those rows - * column_profile: list of the first r independent column indices in ``pivot_matrix`` - where r is the rank of `K`. - - EXAMPLES: - - sage: R. = GF(97)[] - sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: E.striped_krylov_matrix(J,3) - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3) - ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[0,3,6]) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3,[0,3,6]) - ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[3,0,2]) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3,[3,0,2]) - ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) - ) - """ - from sage.matrix.constructor import matrix - import math - from sage.combinat.permutation import Permutation - - m = self.nrows() - if m == 0: return (),self,() From 3cc05bd13300c42819d92f224ee49b6b5ac42896 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 3 Jul 2025 12:27:59 +0200 Subject: [PATCH 12/57] optimised version of basis calculation without permutation --- src/sage/matrix/matrix2.pyx | 104 +++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 779cf473084..df4f389eedf 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19235,7 +19235,7 @@ cdef class Matrix(Matrix1): return row_profile,pivot,col_profile - def krylov_rank_profile(self, J, degree, shift=None): + def krylov_rank_profile(self, J, degree, shift=None, output_pairs=False): r""" Compute the rank profile (row and column) of the striped Krylov matrix built from ``self`` and matrix `J`. @@ -19376,11 +19376,16 @@ cdef class Matrix(Matrix1): # calculate new M for return value or next loop M = matrix([M.row(i) for i in row_profile_M]) + + col_profile = M.col_rank_profile() + + if output_pairs: + return tuple(row_profile_self), M, col_profile + # convert c,d to actual position in striped Krylov matrix phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(len(shift))) row_profile_K = [phi(*row) for row in row_profile_self] - col_profile = M.col_rank_profile() return tuple(row_profile_K), M, col_profile def linear_interpolation_basis(self, J, degree, var_name, shift=None): @@ -19476,6 +19481,101 @@ cdef class Matrix(Matrix1): basis_rows[i][i] += variable**degree_c[i] return matrix(basis_rows) + + def linear_interpolation_basis_fast_perm(self, J, degree, var_name, shift=None): + r""" + Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. + + INPUT: + + - ``J`` -- square matrix for Krylov construction. + - ``degree`` -- upper bound on degree of minpoly(`J`), power of + - ``shift`` -- list of integers (optional): controls priority of rows. + + OUTPUT: + + - Matrix whose rows form an interpolation basis. + + """ + from sage.combinat.permutation import Permutation + from sage.matrix.constructor import matrix + from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing + + poly_ring = PolynomialRing(self.base_ring(),var_name) + + m = self.nrows() + + # if no shift, this is equivalent to 0s + if shift is None: + shift = [0]*self.nrows() + + # # calculate shift priority function and permutation + # priority = lambda c,d : shift[c] + d + # index = lambda i : (i%m,i//m) + # index_inv = lambda c,d : c + d*m + + # priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + # priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + # priority_permutation_inv = priority_permutation.inverse() + + # # maps row c of EJ^d to position i in striped Krylov matrix + # # +/- 1 as permutations are 1-indexed, matrices are 0-indexed + # phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 + # phi_inv = lambda i : index(priority_permutation(i + 1) - 1) + + # calculate krylov profile + row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift,output_pairs=True) + + if len(row_profile) == 0: + return matrix.identity(poly_ring,m) + + # (c_k, d_k) = phi^-1 (row_i) + #c, d = zip(*(phi_inv(i) for i in row_profile)) + c, d = zip(*(row for row in row_profile)) + + # degree_c = 0 or max d[k] such that c[k] = c + inv_c = [None]*m + degree_c = [0]*m + for k in range(len(row_profile)): + if d[k] >= degree_c[c[k]]: + degree_c[c[k]] = d[k] + 1 + inv_c[c[k]] = k + + T_absent_indices = [i for i in range(m) if inv_c[i] is None] + T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] + D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) + D_present = (pivot.matrix_from_rows(T_present_indices)*J).matrix_from_columns(col_profile) + D_rows = [] + idx_p = 0 + idx_a = 0 + for i in range(m): + if inv_c[i] is None: + D_rows.append(D_absent[idx_a]) + idx_a += 1 + else: + D_rows.append(D_present[idx_p]) + idx_p += 1 + + C = pivot.matrix_from_columns(col_profile) + D = matrix(D_rows) + + relation = D*C.inverse() + + # linear interpolation basis in shifted Popov form + uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) + + # construct variable + variable = poly_ring.gen() + + # compression of basis into polynomial form + basis_rows = [[0]*m for i in range(m)] + for col in range(relation.ncols()): + for row in range(m): + basis_rows[row][c[col]] += uncompressed_basis[row][col] * variable**d[col] + for i in range(m): + basis_rows[i][i] += variable**degree_c[i] + + return matrix(basis_rows) # a limited number of access-only properties are provided for matrices @property From 59dea5739548c3749dc219417229368667b1e203 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 3 Jul 2025 12:31:48 +0200 Subject: [PATCH 13/57] make optimisation final --- src/sage/matrix/matrix2.pyx | 96 +------------------------------------ 1 file changed, 1 insertion(+), 95 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index df4f389eedf..125fdeb1af3 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19387,102 +19387,8 @@ cdef class Matrix(Matrix1): row_profile_K = [phi(*row) for row in row_profile_self] return tuple(row_profile_K), M, col_profile - - def linear_interpolation_basis(self, J, degree, var_name, shift=None): - r""" - Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. - - INPUT: - - - ``J`` -- square matrix for Krylov construction. - - ``degree`` -- upper bound on degree of minpoly(`J`), power of - - ``shift`` -- list of integers (optional): controls priority of rows. - OUTPUT: - - - Matrix whose rows form an interpolation basis. - - """ - from sage.combinat.permutation import Permutation - from sage.matrix.constructor import matrix - from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - - poly_ring = PolynomialRing(self.base_ring(),var_name) - - m = self.nrows() - - # if no shift, this is equivalent to 0s - if shift is None: - shift = [0]*self.nrows() - - # calculate shift priority function and permutation - priority = lambda c,d : shift[c] + d - index = lambda i : (i%m,i//m) - index_inv = lambda c,d : c + d*m - - priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) - priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - priority_permutation_inv = priority_permutation.inverse() - - # maps row c of EJ^d to position i in striped Krylov matrix - # +/- 1 as permutations are 1-indexed, matrices are 0-indexed - phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 - phi_inv = lambda i : index(priority_permutation(i + 1) - 1) - - # calculate krylov profile - row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift) - - if len(row_profile) == 0: - return matrix.identity(poly_ring,m) - - # (c_k, d_k) = phi^-1 (row_i) - c, d = zip(*(phi_inv(i) for i in row_profile)) - - # degree_c = 0 or max d[k] such that c[k] = c - inv_c = [None]*m - degree_c = [0]*m - for k in range(len(row_profile)): - if d[k] >= degree_c[c[k]]: - degree_c[c[k]] = d[k] + 1 - inv_c[c[k]] = k - - T_absent_indices = [i for i in range(m) if inv_c[i] is None] - T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] - D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) - D_present = (pivot.matrix_from_rows(T_present_indices)*J).matrix_from_columns(col_profile) - D_rows = [] - idx_p = 0 - idx_a = 0 - for i in range(m): - if inv_c[i] is None: - D_rows.append(D_absent[idx_a]) - idx_a += 1 - else: - D_rows.append(D_present[idx_p]) - idx_p += 1 - - C = pivot.matrix_from_columns(col_profile) - D = matrix(D_rows) - - relation = D*C.inverse() - - # linear interpolation basis in shifted Popov form - uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) - - # construct variable - variable = poly_ring.gen() - - # compression of basis into polynomial form - basis_rows = [[0]*m for i in range(m)] - for col in range(relation.ncols()): - for row in range(m): - basis_rows[row][c[col]] += uncompressed_basis[row][col] * variable**d[col] - for i in range(m): - basis_rows[i][i] += variable**degree_c[i] - - return matrix(basis_rows) - - def linear_interpolation_basis_fast_perm(self, J, degree, var_name, shift=None): + def linear_interpolation_basis(self, J, degree, var_name, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. From ae04c03f4cd4cdba5c60d776f654385b8e097b07 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 4 Jul 2025 13:12:14 +0200 Subject: [PATCH 14/57] optimise matrix creation for basis, fix echelon_form bug --- src/sage/matrix/matrix2.pyx | 49 ++++++++++--------- .../matrix/matrix_modn_dense_template.pxi | 2 +- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 125fdeb1af3..a6ef7f307ce 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19415,20 +19415,6 @@ cdef class Matrix(Matrix1): if shift is None: shift = [0]*self.nrows() - # # calculate shift priority function and permutation - # priority = lambda c,d : shift[c] + d - # index = lambda i : (i%m,i//m) - # index_inv = lambda c,d : c + d*m - - # priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) - # priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - # priority_permutation_inv = priority_permutation.inverse() - - # # maps row c of EJ^d to position i in striped Krylov matrix - # # +/- 1 as permutations are 1-indexed, matrices are 0-indexed - # phi = lambda c,d : priority_permutation_inv(index_inv(c,d) + 1) - 1 - # phi_inv = lambda i : index(priority_permutation(i + 1) - 1) - # calculate krylov profile row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift,output_pairs=True) @@ -19467,19 +19453,36 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() - # linear interpolation basis in shifted Popov form - uncompressed_basis = matrix.block([[-relation,matrix.identity(m)]],subdivide=False) + coeffs_map = {} + for i in range(m): + coeffs_map[i] = {} + for j in range(m): + coeffs_map[i][j] = {} - # construct variable - variable = poly_ring.gen() + for i in range(m): + coeffs_map[i][i][degree_c[i]] = poly_ring.base_ring().one() - # compression of basis into polynomial form - basis_rows = [[0]*m for i in range(m)] for col in range(relation.ncols()): for row in range(m): - basis_rows[row][c[col]] += uncompressed_basis[row][col] * variable**d[col] - for i in range(m): - basis_rows[i][i] += variable**degree_c[i] + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col],poly_ring.base_ring().zero()) - relation[row][col] + + basis_rows = [[None for _ in range(m)] for _ in range(m)] + + monomial_cache = {} + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = poly_ring.zero() + elif len(coeffs_map[row][col]) == 1: + deg, coeff = coeffs_map[row][col].popitem() + if coeff not in monomial_cache: + monomial_cache[coeff] = poly_ring(coeff) + basis_rows[row][col] = monomial_cache[coeff].shift(deg) + else: + coeffs = [poly_ring.base_ring().zero()] * (max(coeffs_map[row][col].keys()) + 1) + for deg, coeff in coeffs_map[row][col].items(): + coeffs[deg] = coeff + basis_rows[row][col] = poly_ring(coeffs) return matrix(basis_rows) diff --git a/src/sage/matrix/matrix_modn_dense_template.pxi b/src/sage/matrix/matrix_modn_dense_template.pxi index 63fac0af4b0..4655b3be180 100644 --- a/src/sage/matrix/matrix_modn_dense_template.pxi +++ b/src/sage/matrix/matrix_modn_dense_template.pxi @@ -176,7 +176,7 @@ cdef inline linbox_echelonize(celement modulus, celement* entries, Py_ssize_t nr Return the reduced row echelon form of this matrix. """ if linbox_is_zero(modulus, entries, nrows, ncols): - return 0, [] + return 0, [], [] cdef Py_ssize_t i, j cdef ModField *F = new ModField(modulus) From 796155e249576d91cfa18da43cb6e1de75076e53 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Tue, 8 Jul 2025 15:52:52 +0200 Subject: [PATCH 15/57] use matrix_from_rows in krylov_rank_profile, optimisation for polynomial creation in basis calculation --- src/sage/matrix/matrix2.pyx | 104 ++++++++++++++++++++++++------------ 1 file changed, 69 insertions(+), 35 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index a6ef7f307ce..5f8ad5fb952 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19349,7 +19349,7 @@ cdef class Matrix(Matrix1): M = self_permuted.matrix_from_rows(row_profile_self_permuted) J_L = None - + for l in range(math.ceil(math.log(degree,2)) + 1): L = pow(2,l) # adding 2^l to each degree @@ -19366,16 +19366,19 @@ cdef class Matrix(Matrix1): J_L = J else: J_L = J_L * J_L + M = M.stack(M*J_L,subdivide=False) # sort rows of M, find profile, translate to k (indices of full krylov matrix) M.permute_rows(k_perm) + row_profile_M = M.row_rank_profile() + r = len(row_profile_M) row_profile_self = sorted([k[k_perm(i+1)-1] for i in row_profile_M],key=lambda x: (shift[x[0]]+x[1],x[0])) # calculate new M for return value or next loop - M = matrix([M.row(i) for i in row_profile_M]) + M = M.matrix_from_rows(row_profile_M) col_profile = M.col_rank_profile() @@ -19383,12 +19386,12 @@ cdef class Matrix(Matrix1): return tuple(row_profile_self), M, col_profile # convert c,d to actual position in striped Krylov matrix - phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(len(shift))) + phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) row_profile_K = [phi(*row) for row in row_profile_self] return tuple(row_profile_K), M, col_profile - def linear_interpolation_basis(self, J, degree, var_name, shift=None): + def linear_interpolation_basis(self, J, degree, var_name, shift=None, polynomial_output=True): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. @@ -19449,42 +19452,73 @@ cdef class Matrix(Matrix1): idx_p += 1 C = pivot.matrix_from_columns(col_profile) + D = matrix(D_rows) relation = D*C.inverse() - coeffs_map = {} - for i in range(m): - coeffs_map[i] = {} - for j in range(m): - coeffs_map[i][j] = {} - - for i in range(m): - coeffs_map[i][i][degree_c[i]] = poly_ring.base_ring().one() - - for col in range(relation.ncols()): - for row in range(m): - coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col],poly_ring.base_ring().zero()) - relation[row][col] - - basis_rows = [[None for _ in range(m)] for _ in range(m)] - - monomial_cache = {} - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = poly_ring.zero() - elif len(coeffs_map[row][col]) == 1: - deg, coeff = coeffs_map[row][col].popitem() - if coeff not in monomial_cache: - monomial_cache[coeff] = poly_ring(coeff) - basis_rows[row][col] = monomial_cache[coeff].shift(deg) - else: - coeffs = [poly_ring.base_ring().zero()] * (max(coeffs_map[row][col].keys()) + 1) - for deg, coeff in coeffs_map[row][col].items(): - coeffs[deg] = coeff - basis_rows[row][col] = poly_ring(coeffs) + if polynomial_output: + # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields + coeffs_map = [[{} for _ in range(m)] for _ in range(m)] + # add identity part of basis + for i in range(m): + coeffs_map[i][i][degree_c[i]] = poly_ring.base_ring().one() + # add relation part of basis + for col in range(relation.ncols()): + for row in range(m): + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col],poly_ring.base_ring().zero()) - relation[row,col] + # construct rows + basis_rows = [[None for _ in range(m)] for _ in range(m)] + if poly_ring.base_ring().degree() <= 1 or m*m > poly_ring.base_ring().order(): + # if base field is large, don't bother caching monomials + # or if polynomial construction is efficient (order = 1) + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = poly_ring.base_ring().zero() + else: + # construct a small polynomial (less costly), then shift + mindeg = min(coeffs_map[row][col].keys()) + maxdeg = max(coeffs_map[row][col].keys()) + entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) + for deg, coeff in coeffs_map[row][col].items(): + entries[deg-mindeg] = coeff + basis_rows[row][col] = poly_ring(entries).shift(mindeg) + else: + # if base field is small, cache monomials to minimise number of constructions + monomial_cache = {} + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = poly_ring.zero() + elif len(coeffs_map[row][col]) == 1: + # monomial case + deg, coeff = coeffs_map[row][col].popitem() + if coeff not in monomial_cache: + monomial_cache[coeff] = poly_ring(coeff) + basis_rows[row][col] = monomial_cache[coeff].shift(deg) + else: + # dense case + mindeg = min(coeffs_map[row][col].keys()) + maxdeg = max(coeffs_map[row][col].keys()) + entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) + for deg, coeff in coeffs_map[row][col].items(): + entries[deg-mindeg] = coeff + basis_rows[row][col] = poly_ring(entries).shift(mindeg) + # convert to matrix + output = matrix(poly_ring,basis_rows) + else: + # expected output is expanded matrix + row_width = m * (max(degree_c) + 1) + basis_rows = [[poly_ring.base_ring().zero()]*row_width for i in range(m)] + for i in range(m): + basis_rows[i][i+degree_c[i]*m] = poly_ring.base_ring().one() + for col in range(relation.ncols()): + for row in range(m): + basis_rows[row][c[col]+d[col]*m] -= relation[row,col] + output = matrix(poly_ring.base_ring(),basis_rows) - return matrix(basis_rows) + return output # a limited number of access-only properties are provided for matrices @property From a6de9529bf3a25a259eb0914a7b7d6589c62e00d Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Tue, 8 Jul 2025 16:44:37 +0200 Subject: [PATCH 16/57] add early exit filtering for krylov rank profile --- src/sage/matrix/matrix2.pyx | 180 ++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 5f8ad5fb952..2d6de67ce9a 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19391,6 +19391,186 @@ cdef class Matrix(Matrix1): return tuple(row_profile_K), M, col_profile + def krylov_rank_profile_early_exit(self, J, degree, shift=None, output_pairs=False): + r""" + Compute the rank profile (row and column) of the striped Krylov matrix + built from ``self`` and matrix `J`. + + INPUT: + + - ``J`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of J in Krylov matrix. + - ``shift`` -- list of integers (optional): priority row shift. + + OUTPUT: + + - A tuple (row_profile, pivot_matrix, column_profile): + * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows + * pivot: the submatrix of `K` given by those rows + * column_profile: list of the first r independent column indices in ``pivot_matrix`` + where r is the rank of `K`. + + EXAMPLES: + + sage: R. = GF(97)[] + sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(J,3) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3,[0,3,6]) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.striped_krylov_matrix(J,3,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,3,[3,0,2]) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + """ + from sage.matrix.constructor import matrix + import math + from sage.combinat.permutation import Permutation + + m = self.nrows() + + if m == 0: + return (),self,() + + # calculate row profile of self, with shift applied + self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) + + self_permuted = self.with_permuted_rows(self_permutation) + row_profile_self_permuted = self_permuted.row_rank_profile() + row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] + + exhausted = matrix.zero(self.base_ring(), 0, self.ncols()) + row_profile_exhausted = [] + excluded_rows = set() + + r = len(row_profile_self) + + if r == 0: + return (),self.matrix_from_rows([]),() + + M = self_permuted.matrix_from_rows(row_profile_self_permuted) + + J_L = None + + for l in range(math.ceil(math.log(degree,2)) + 1): + L = pow(2,l) + # adding 2^l to each degree + row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] + if len(row_extension) == 0: + break + + # concatenate two sequences (order preserved) + k = row_profile_exhausted + row_profile_self + row_extension + + # calculate sorting permutation, sort k by (shift[c]+d, c) + k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + + # fast calculation of rows formed by indices in k + if J_L is None: + J_L = J + else: + J_L = J_L * J_L + + M = matrix.block([[exhausted],[M],[M*J_L]],subdivide=False) + + # sort rows of M, find profile, translate to k (indices of full krylov matrix) + M.permute_rows(k_perm) + + row_profile_M = M.row_rank_profile() + r = len(row_profile_M) + + if r == self.ncols(): + tail = list(range(row_profile_M[-1]+1,M.nrows())) + excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) + + xmi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] in excluded_rows] + imi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] not in excluded_rows] + + row_profile_exhausted = [k[k_perm(i+1)-1] for i in xmi] + row_profile_self = [k[k_perm(i+1)-1] for i in imi] + + exhausted = M.matrix_from_rows(xmi) + M = M.matrix_from_rows(imi) + else: + row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_M] + # calculate new M for return value or next loop + M = M.matrix_from_rows(row_profile_M) + if exhausted.nrows() != 0: + k = row_profile_exhausted + row_profile_self + M = exhausted.stack(M) + k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + + M.permute_rows(k_perm) + row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] + col_profile = M.col_rank_profile() + + if output_pairs: + return tuple(row_profile_self), M, col_profile + + # convert c,d to actual position in striped Krylov matrix + phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) + row_profile_K = [phi(*row) for row in row_profile_self] + + return tuple(row_profile_K), M, col_profile + def linear_interpolation_basis(self, J, degree, var_name, shift=None, polynomial_output=True): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. From a010078eef0040fcd1a9116c1ef55f766c09d83a Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 9 Jul 2025 13:55:09 +0200 Subject: [PATCH 17/57] mitigate performance loss on hermite shift --- src/sage/matrix/matrix2.pyx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 2d6de67ce9a..4df0bc16bd8 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19537,7 +19537,7 @@ cdef class Matrix(Matrix1): row_profile_M = M.row_rank_profile() r = len(row_profile_M) - if r == self.ncols(): + if r == self.ncols() and l < math.ceil(math.log(degree,2)): tail = list(range(row_profile_M[-1]+1,M.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) @@ -19550,10 +19550,16 @@ cdef class Matrix(Matrix1): exhausted = M.matrix_from_rows(xmi) M = M.matrix_from_rows(imi) else: + if l == math.ceil(math.log(degree,2)): + row_profile_exhausted = [] + exhausted = matrix.zero(self.base_ring(), 0, self.ncols()) row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_M] # calculate new M for return value or next loop M = M.matrix_from_rows(row_profile_M) - if exhausted.nrows() != 0: + if M.nrows() == 0: + M = exhausted + row_profile_self = row_profile_exhausted + elif exhausted.nrows() != 0: k = row_profile_exhausted + row_profile_self M = exhausted.stack(M) k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) From b6df7d32c3b1fa72a49fb3154fdef1a327d40247 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 17 Jul 2025 20:07:08 +0200 Subject: [PATCH 18/57] make early exit permanent --- src/sage/matrix/matrix2.pyx | 158 +----------------------------------- 1 file changed, 1 insertion(+), 157 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 13303ce0fbd..6dbc119ed8e 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19319,164 +19319,8 @@ cdef class Matrix(Matrix1): col_profile = pivot.col_rank_profile() return row_profile,pivot,col_profile - - def krylov_rank_profile(self, J, degree, shift=None, output_pairs=False): - r""" - Compute the rank profile (row and column) of the striped Krylov matrix - built from ``self`` and matrix `J`. - - INPUT: - - - ``J`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of J in Krylov matrix. - - ``shift`` -- list of integers (optional): priority row shift. - - OUTPUT: - - - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows - * pivot: the submatrix of `K` given by those rows - * column_profile: list of the first r independent column indices in ``pivot_matrix`` - where r is the rank of `K`. - - EXAMPLES: - - sage: R. = GF(97)[] - sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: E.striped_krylov_matrix(J,3) - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3) - ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[0,3,6]) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3,[0,3,6]) - ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[3,0,2]) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J,3,[3,0,2]) - ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) - ) - """ - from sage.matrix.constructor import matrix - import math - from sage.combinat.permutation import Permutation - - m = self.nrows() - - if m == 0: - return (),self,() - - # calculate row profile of self, with shift applied - self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) - - self_permuted = self.with_permuted_rows(self_permutation) - row_profile_self_permuted = self_permuted.row_rank_profile() - row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] - - r = len(row_profile_self) - - if r == 0: - return (),self.matrix_from_rows([]),() - - M = self_permuted.matrix_from_rows(row_profile_self_permuted) - - J_L = None - - for l in range(math.ceil(math.log(degree,2)) + 1): - L = pow(2,l) - # adding 2^l to each degree - row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] - - # concatenate two sequences (order preserved) - k = row_profile_self + row_extension - - # calculate sorting permutation, sort k by (shift[c]+d, c) - k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) - - # fast calculation of rows formed by indices in k - if J_L is None: - J_L = J - else: - J_L = J_L * J_L - - M = M.stack(M*J_L,subdivide=False) - - # sort rows of M, find profile, translate to k (indices of full krylov matrix) - M.permute_rows(k_perm) - - row_profile_M = M.row_rank_profile() - - r = len(row_profile_M) - row_profile_self = sorted([k[k_perm(i+1)-1] for i in row_profile_M],key=lambda x: (shift[x[0]]+x[1],x[0])) - - # calculate new M for return value or next loop - M = M.matrix_from_rows(row_profile_M) - - col_profile = M.col_rank_profile() - - if output_pairs: - return tuple(row_profile_self), M, col_profile - - # convert c,d to actual position in striped Krylov matrix - phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) - row_profile_K = [phi(*row) for row in row_profile_self] - - return tuple(row_profile_K), M, col_profile - def krylov_rank_profile_early_exit(self, J, degree, shift=None, output_pairs=False): + def krylov_rank_profile(self, J, degree, shift=None, output_pairs=False): r""" Compute the rank profile (row and column) of the striped Krylov matrix built from ``self`` and matrix `J`. From 661db965d4dbd803a7df1bbe59ff5a4cd115cd8a Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 21 Jul 2025 12:18:46 +0200 Subject: [PATCH 19/57] fix boundary conditions for polynomial conversion --- src/sage/matrix/matrix2.pyx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 6dbc119ed8e..64c5431809a 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19584,7 +19584,7 @@ cdef class Matrix(Matrix1): coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col],poly_ring.base_ring().zero()) - relation[row,col] # construct rows basis_rows = [[None for _ in range(m)] for _ in range(m)] - if poly_ring.base_ring().degree() <= 1 or m*m > poly_ring.base_ring().order(): + if poly_ring.base_ring().degree() <= 1 or m*m*self.ncols() < poly_ring.base_ring().order(): # if base field is large, don't bother caching monomials # or if polynomial construction is efficient (order = 1) for row in range(m): @@ -19613,13 +19613,19 @@ cdef class Matrix(Matrix1): monomial_cache[coeff] = poly_ring(coeff) basis_rows[row][col] = monomial_cache[coeff].shift(deg) else: - # dense case + # general case mindeg = min(coeffs_map[row][col].keys()) - maxdeg = max(coeffs_map[row][col].keys()) - entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) + # maxdeg = max(coeffs_map[row][col].keys()) + poly = poly_ring.zero() for deg, coeff in coeffs_map[row][col].items(): - entries[deg-mindeg] = coeff - basis_rows[row][col] = poly_ring(entries).shift(mindeg) + if coeff not in monomial_cache: + monomial_cache[coeff] = poly_ring(coeff) + poly += monomial_cache[coeff].shift(deg-mindeg) + basis_rows[row][col] = poly.shift(mindeg) + # entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) + # for deg, coeff in coeffs_map[row][col].items(): + # entries[deg-mindeg] = coeff + # basis_rows[row][col] = poly_ring(entries).shift(mindeg) # convert to matrix output = matrix(poly_ring,basis_rows) else: From 01f9301c8f0ab9d78e57528e61621703e71b68fa Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 28 Jul 2025 15:41:19 +0200 Subject: [PATCH 20/57] tidy up code --- src/sage/matrix/matrix2.pyx | 346 ++++++++++++++++-------------------- 1 file changed, 152 insertions(+), 194 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 64c5431809a..fc101b3000a 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19040,60 +19040,11 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def polynomial_compression(self,degree,var_name): - """ - Returns the corresponding polynomial matrix where the coefficient matrix of degree i is the ith block of ``self``. - - INPUT: - - - ``degree`` -- integer: the number of blocks in the expanded matrix. If ``self`` does not have zero-blocks, - this corresponds to the degree of the resulting matrix. - - ``variable`` -- a symbolic variable (e.g., ``x``) to use in the polynomial. - - OUTPUT: - - - a polynomial matrix with the same number of rows and self.ncols()//(degree+1) columns - - EXAMPLES: - - The first 3 x 3 block of N corresponds to the - - sage: R. = GF(97)[] - sage: S. = ZZ[] - sage: M = matrix([[x^2+36*x,31*x,0],[3*x+13,x+57,0],[96,96,1]]) - sage: M - [x^2 + 36*x 31*x 0] - [ 3*x + 13 x + 57 0] - [ 96 96 1] - sage: N = M.expansion() - sage: N - [ 0 0 0 36 31 0 1 0 0] - [13 57 0 3 1 0 0 0 0] - [96 96 1 0 0 0 0 0 0] - sage: N.polynomial_compression(2,x) - [x^2 + 36*x 31*x 0] - [ 3*x + 13 x + 57 0] - [ 96 96 1] - sage: N.polynomial_compression(2,y) - [y^2 + 36*y 31*y 0] - [ 3*y + 13 y + 57 0] - [ 96 96 1] - """ - from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - - poly_ring = PolynomialRing(self.base_ring(),var_name) - variable = poly_ring.gen() - - if self.ncols() % (degree+1) != 0: - raise ValueError('The column number must be divisible by degree+1.') - coeff_matrices = [self[:,i*(self.ncols()//(degree+1)):(i+1)*(self.ncols()//(degree+1))] for i in range(degree+1)] - return sum([coeff_matrices[i]*variable**i for i in range(degree+1)]) - def striped_krylov_matrix(self, J, degree, shift=None): r""" Return the block matrix of the following form, with rows permuted according to shift. The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. - + [ ] [ E ] [ ] @@ -19109,20 +19060,20 @@ cdef class Matrix(Matrix1): [ ] [ EJ^d] [ ] - + INPUT: - + - ``J`` -- a square matrix of size equal to the number of columns of ``self``. - ``degree`` -- integer, the maximum exponent for the Krylov matrix. - ``shift`` -- list of integers (optional), row priority shifts. If ``None``, defaults to all zero. - + OUTPUT: - A matrix with block rows [E, EJ, EJ^2, ..., EJ^d], row-permuted by shift. - + EXAMPLES: - + sage: R. = GF(97)[] sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) sage: E @@ -19173,48 +19124,48 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - + """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix # for matrix.block - + m = self.nrows() - + # if no shift, this is equivalent to 0s if shift is None: shift = [0]*self.nrows() - + # size checks if len(shift) != m: raise ValueError(f"The shift should have the same number of elements as the rows of the matrix ({m}), but had {len(shift)}.") if not J.is_square() or J.nrows() != self.ncols(): raise ValueError(f"The matrix J should be a square matrix and match the number of columns of self ({self.ncols()}), but is of dimension {J.nrows()} x {J.ncols()}.") - + # define priority and indexing functions # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] priority = lambda c,d : shift[c] + d index = lambda i : (i%m,i//m) - + # deduce permutation by sorting priorities # rows should be ordered by ascending priority, then by associated row number in E (i%m) priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - + # build all blocks for the striped matrix blocks = [] - + # for i from 0 to degree (inclusive), add E*J^i J_i = matrix.identity(self.ncols()) for i in range(degree+1): blocks.append([self*J_i]) if i < degree: J_i *= J - + # return block matrix permuted according to shift krylov = matrix.block(blocks,subdivide=False) krylov.permute_rows(priority_permutation) return krylov - + def naive_krylov_rank_profile(self, J, degree, shift=None): r""" Compute the rank profile (row and column) of the striped Krylov matrix @@ -19233,9 +19184,9 @@ cdef class Matrix(Matrix1): * pivot: the submatrix of `K` given by those rows * column_profile: list of the first r independent column indices in ``pivot_matrix`` where r is the rank of `K`. - + EXAMPLES: - + sage: R. = GF(97)[] sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) sage: E @@ -19306,21 +19257,21 @@ cdef class Matrix(Matrix1): ) """ from sage.matrix.constructor import matrix - + K = self.striped_krylov_matrix(J,degree,shift) - + # calculate first independent rows of K row_profile = K.row_rank_profile() - + # construct submatrix pivot = K.matrix_from_rows(row_profile) - + # calculate first independent columns of pivot col_profile = pivot.col_rank_profile() - + return row_profile,pivot,col_profile - def krylov_rank_profile(self, J, degree, shift=None, output_pairs=False): + def krylov_rank_profile(self, J, degree=None, shift=None, output_pairs=False): r""" Compute the rank profile (row and column) of the striped Krylov matrix built from ``self`` and matrix `J`. @@ -19339,10 +19290,10 @@ cdef class Matrix(Matrix1): * column_profile: list of the first r independent column indices in ``pivot_matrix`` where r is the rank of `K`. - EXAMPLES: + TESTS:: - sage: R. = GF(97)[] - sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) sage: E [27 49 29] [50 58 0] @@ -19352,70 +19303,71 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: E.striped_krylov_matrix(J,3) - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] sage: E.krylov_rank_profile(J,3) ( [27 49 29] [50 58 0] (0, 1, 3), [ 0 27 49], (0, 1, 2) ) - sage: E.striped_krylov_matrix(J,3,[0,3,6]) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] + sage: E.krylov_rank_profile(J,3,None,True) + ( + [27 49 29] + [50 58 0] + ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) + ) sage: E.krylov_rank_profile(J,3,[0,3,6]) ( [27 49 29] [ 0 27 49] (0, 1, 2), [ 0 0 27], (0, 1, 2) ) - sage: E.striped_krylov_matrix(J,3,[3,0,2]) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] + sage: E.krylov_rank_profile(J,3,[0,3,6],True) + ( + [27 49 29] + [ 0 27 49] + ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) + ) sage: E.krylov_rank_profile(J,3,[3,0,2]) ( [50 58 0] [ 0 50 58] (0, 1, 2), [ 0 0 50], (0, 1, 2) ) + sage: E.krylov_rank_profile(J,3,[3,0,2],True) + ( + [50 58 0] + [ 0 50 58] + ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) + ) """ from sage.matrix.constructor import matrix import math from sage.combinat.permutation import Permutation - + + if not isinstance(J, Matrix): + raise TypeError("krylov_rank_profile: J is not a matrix") + if J.nrows() != self.ncols() or J.ncols() != self.ncols(): + raise ValueError("krylov_rank_profile: matrix J does not have correct dimensions.") + if J.base_ring() != self.base_ring(): + raise ValueError("krylov_rank_profile: matrix J does not have same base ring as E.") + + if degree is None: + degree = self.ncols() + if not isinstance(degree, (int, sage.rings.integer.Integer)): + raise TypeError("krylov_rank_profile: degree is not an integer.") + if degree < 0: + raise ValueError("krylov_rank_profile: degree bound cannot be negative.") + + if shift is None or shift == 0: + shift = (ZZ**self.nrows()).zero() + if isinstance(shift, (list, tuple)): + shift = (ZZ**self.nrows())(shift) + if shift not in ZZ**self.nrows(): + raise ValueError(f"krylov_rank_profile: shift is not a Z-vector of length {self.nrows()}.") + m = self.nrows() - + sigma = self.ncols() + if m == 0: return (),self,() @@ -19426,7 +19378,7 @@ cdef class Matrix(Matrix1): row_profile_self_permuted = self_permuted.row_rank_profile() row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] - exhausted = matrix.zero(self.base_ring(), 0, self.ncols()) + exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_exhausted = [] excluded_rows = set() @@ -19466,7 +19418,7 @@ cdef class Matrix(Matrix1): row_profile_M = M.row_rank_profile() r = len(row_profile_M) - if r == self.ncols() and l < math.ceil(math.log(degree,2)): + if r == sigma and l < math.ceil(math.log(degree,2)): tail = list(range(row_profile_M[-1]+1,M.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) @@ -19481,7 +19433,7 @@ cdef class Matrix(Matrix1): else: if l == math.ceil(math.log(degree,2)): row_profile_exhausted = [] - exhausted = matrix.zero(self.base_ring(), 0, self.ncols()) + exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_M] # calculate new M for return value or next loop M = M.matrix_from_rows(row_profile_M) @@ -19506,13 +19458,14 @@ cdef class Matrix(Matrix1): return tuple(row_profile_K), M, col_profile - def linear_interpolation_basis(self, J, degree, var_name, shift=None, polynomial_output=True): + def linear_interpolation_basis(self, J, var_name, degree=None, shift=None): r""" Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. INPUT: - ``J`` -- square matrix for Krylov construction. + - ``var_name`` -- variable name for the returned polynomial matrix - ``degree`` -- upper bound on degree of minpoly(`J`), power of - ``shift`` -- list of integers (optional): controls priority of rows. @@ -19524,18 +19477,40 @@ cdef class Matrix(Matrix1): from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing - - poly_ring = PolynomialRing(self.base_ring(),var_name) - + + # INPUT VALIDATION + if not isinstance(J, Matrix): + raise TypeError("krylov_rank_profile: J is not a matrix") + if J.nrows() != self.ncols() or J.ncols() != self.ncols(): + raise ValueError("krylov_rank_profile: matrix J does not have correct dimensions.") + if J.base_ring() != self.base_ring(): + raise ValueError("krylov_rank_profile: matrix J does not have same base ring as E.") + + if not isinstance(var_name, str): + raise TypeError("var_name is not a string") + + if degree is None: + degree = self.ncols() + if not isinstance(degree, (int, sage.rings.integer.Integer)): + raise TypeError("krylov_rank_profile: degree is not an integer.") + if degree < 0: + raise ValueError("krylov_rank_profile: degree bound cannot be negative.") + + if shift is None or shift == 0: + shift = (ZZ**self.nrows()).zero() + if isinstance(shift, (list, tuple)): + shift = (ZZ**self.nrows())(shift) + if shift not in ZZ**self.nrows(): + raise ValueError(f"krylov_rank_profile: shift is not a Z-vector of length {self.nrows()}.") + m = self.nrows() + sigma = self.ncols() - # if no shift, this is equivalent to 0s - if shift is None: - shift = [0]*self.nrows() + poly_ring = PolynomialRing(self.base_ring(),var_name) # calculate krylov profile row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift,output_pairs=True) - + if len(row_profile) == 0: return matrix.identity(poly_ring,m) @@ -19572,74 +19547,57 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() - if polynomial_output: - # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields - coeffs_map = [[{} for _ in range(m)] for _ in range(m)] - # add identity part of basis - for i in range(m): - coeffs_map[i][i][degree_c[i]] = poly_ring.base_ring().one() - # add relation part of basis - for col in range(relation.ncols()): - for row in range(m): - coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col],poly_ring.base_ring().zero()) - relation[row,col] - # construct rows - basis_rows = [[None for _ in range(m)] for _ in range(m)] - if poly_ring.base_ring().degree() <= 1 or m*m*self.ncols() < poly_ring.base_ring().order(): - # if base field is large, don't bother caching monomials - # or if polynomial construction is efficient (order = 1) - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = poly_ring.base_ring().zero() - else: - # construct a small polynomial (less costly), then shift - mindeg = min(coeffs_map[row][col].keys()) - maxdeg = max(coeffs_map[row][col].keys()) - entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) - for deg, coeff in coeffs_map[row][col].items(): - entries[deg-mindeg] = coeff - basis_rows[row][col] = poly_ring(entries).shift(mindeg) - else: - # if base field is small, cache monomials to minimise number of constructions - monomial_cache = {} - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = poly_ring.zero() - elif len(coeffs_map[row][col]) == 1: - # monomial case - deg, coeff = coeffs_map[row][col].popitem() + # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields + coeffs_map = [[{} for _ in range(m)] for _ in range(m)] + # add identity part of basis + for i in range(m): + coeffs_map[i][i][degree_c[i]] = self.base_ring().one() + # add relation part of basis + for col in range(relation.ncols()): + for row in range(m): + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], self.base_ring().zero()) - relation[row,col] + # construct rows + basis_rows = [[None] * m for _ in range(m)] + if self.base_ring().degree() <= 1 or m*m*sigma < self.base_ring().order(): + # if base field is large, don't bother caching monomials + # or if polynomial construction is efficient (order = 1) + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = self.base_ring().zero() + else: + # construct a small polynomial (less costly), then shift + mindeg = min(coeffs_map[row][col].keys()) + maxdeg = max(coeffs_map[row][col].keys()) + entries = [self.base_ring().zero()] * (maxdeg-mindeg+1) + for deg, coeff in coeffs_map[row][col].items(): + entries[deg-mindeg] = coeff + basis_rows[row][col] = poly_ring(entries).shift(mindeg) + return matrix(poly_ring, basis_rows) + else: + # if base field is small, cache monomials to minimise number of constructions + monomial_cache = {} + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = poly_ring.zero() + elif len(coeffs_map[row][col]) == 1: + # monomial case + deg, coeff = coeffs_map[row][col].popitem() + if coeff not in monomial_cache: + monomial_cache[coeff] = poly_ring(coeff) + basis_rows[row][col] = monomial_cache[coeff].shift(deg) + else: + # general case + mindeg = min(coeffs_map[row][col].keys()) + poly = poly_ring.zero() + for deg, coeff in coeffs_map[row][col].items(): if coeff not in monomial_cache: monomial_cache[coeff] = poly_ring(coeff) - basis_rows[row][col] = monomial_cache[coeff].shift(deg) - else: - # general case - mindeg = min(coeffs_map[row][col].keys()) - # maxdeg = max(coeffs_map[row][col].keys()) - poly = poly_ring.zero() - for deg, coeff in coeffs_map[row][col].items(): - if coeff not in monomial_cache: - monomial_cache[coeff] = poly_ring(coeff) - poly += monomial_cache[coeff].shift(deg-mindeg) - basis_rows[row][col] = poly.shift(mindeg) - # entries = [poly_ring.base_ring().zero()] * (maxdeg-mindeg+1) - # for deg, coeff in coeffs_map[row][col].items(): - # entries[deg-mindeg] = coeff - # basis_rows[row][col] = poly_ring(entries).shift(mindeg) - # convert to matrix - output = matrix(poly_ring,basis_rows) - else: - # expected output is expanded matrix - row_width = m * (max(degree_c) + 1) - basis_rows = [[poly_ring.base_ring().zero()]*row_width for i in range(m)] - for i in range(m): - basis_rows[i][i+degree_c[i]*m] = poly_ring.base_ring().one() - for col in range(relation.ncols()): - for row in range(m): - basis_rows[row][c[col]+d[col]*m] -= relation[row,col] - output = matrix(poly_ring.base_ring(),basis_rows) - - return output + poly += monomial_cache[coeff].shift(deg-mindeg) + basis_rows[row][col] = poly.shift(mindeg) + # convert to matrix + return matrix(poly_ring,basis_rows) # a limited number of access-only properties are provided for matrices @property From 926420550c18596b4166a2abb474793d16570b9e Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Tue, 29 Jul 2025 15:55:10 +0200 Subject: [PATCH 21/57] update doc, add tests, remove whitespace --- src/sage/matrix/matrix2.pyx | 257 +++++++++++++++++++++++++++++------- 1 file changed, 211 insertions(+), 46 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index fc101b3000a..3a82627f472 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19285,13 +19285,20 @@ cdef class Matrix(Matrix1): OUTPUT: - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows - * pivot: the submatrix of `K` given by those rows - * column_profile: list of the first r independent column indices in ``pivot_matrix`` + * ``row_profile``: list of the first r row indices in the striped + Krylov matrix ``K`` corresponding to + independent rows. If ``output_pairs`` is True, + then the output is the list of pairs (c_i, d_i) + where c_i is the corresponding row in the + original matrix and d_i the corresponding power + of ``J``. + * ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + * ``column_profile``: the first r independent column indices in + ``pivot_matrix`` where r is the rank of `K`. - + TESTS:: - + sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) sage: E @@ -19303,37 +19310,124 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: E.krylov_rank_profile(J,3) + sage: degree = 3 + sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J) ( [27 49 29] [50 58 0] (0, 1, 3), [ 0 27 49], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,3,None,True) + sage: E.krylov_rank_profile(J,output_pairs=True) ( [27 49 29] [50 58 0] ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,3,[0,3,6]) + + sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] + sage: rows + [(1, 0, 0), + (2, 1, 0), + (3, 2, 0), + (4, 0, 1), + (5, 1, 1), + (6, 2, 1), + (7, 0, 2), + (8, 1, 2), + (9, 2, 2), + (10, 0, 3), + (11, 1, 3), + (12, 2, 3)] + sage: shift = [0,3,6] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(1, 0, 0), + (4, 0, 1), + (7, 0, 2), + (10, 0, 3), + (2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (11, 1, 3), + (3, 2, 0), + (6, 2, 1), + (9, 2, 2), + (12, 2, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_rank_profile(J,shift=shift) ( [27 49 29] [ 0 27 49] (0, 1, 2), [ 0 0 27], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,3,[0,3,6],True) + sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) ( [27 49 29] [ 0 27 49] ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,3,[3,0,2]) + + sage: shift = [3,0,2] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (3, 2, 0), + (1, 0, 0), + (11, 1, 3), + (6, 2, 1), + (4, 0, 1), + (9, 2, 2), + (7, 0, 2), + (12, 2, 3), + (10, 0, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,shift=shift) ( [50 58 0] [ 0 50 58] (0, 1, 2), [ 0 0 50], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,3,[3,0,2],True) + sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) ( [50 58 0] [ 0 50 58] @@ -19370,64 +19464,64 @@ cdef class Matrix(Matrix1): if m == 0: return (),self,() - + # calculate row profile of self, with shift applied self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) - + self_permuted = self.with_permuted_rows(self_permutation) row_profile_self_permuted = self_permuted.row_rank_profile() row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] - + exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_exhausted = [] excluded_rows = set() - + r = len(row_profile_self) - + if r == 0: return (),self.matrix_from_rows([]),() - + M = self_permuted.matrix_from_rows(row_profile_self_permuted) - + J_L = None - + for l in range(math.ceil(math.log(degree,2)) + 1): L = pow(2,l) # adding 2^l to each degree row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] if len(row_extension) == 0: break - + # concatenate two sequences (order preserved) k = row_profile_exhausted + row_profile_self + row_extension - + # calculate sorting permutation, sort k by (shift[c]+d, c) k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) - + # fast calculation of rows formed by indices in k if J_L is None: J_L = J else: J_L = J_L * J_L - + M = matrix.block([[exhausted],[M],[M*J_L]],subdivide=False) - + # sort rows of M, find profile, translate to k (indices of full krylov matrix) M.permute_rows(k_perm) - + row_profile_M = M.row_rank_profile() r = len(row_profile_M) - + if r == sigma and l < math.ceil(math.log(degree,2)): tail = list(range(row_profile_M[-1]+1,M.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) - + xmi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] in excluded_rows] imi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] not in excluded_rows] - + row_profile_exhausted = [k[k_perm(i+1)-1] for i in xmi] row_profile_self = [k[k_perm(i+1)-1] for i in imi] - + exhausted = M.matrix_from_rows(xmi) M = M.matrix_from_rows(imi) else: @@ -19444,35 +19538,108 @@ cdef class Matrix(Matrix1): k = row_profile_exhausted + row_profile_self M = exhausted.stack(M) k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) - + M.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] col_profile = M.col_rank_profile() - + if output_pairs: return tuple(row_profile_self), M, col_profile - + # convert c,d to actual position in striped Krylov matrix phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) row_profile_K = [phi(*row) for row in row_profile_self] - + return tuple(row_profile_K), M, col_profile def linear_interpolation_basis(self, J, var_name, degree=None, shift=None): r""" - Construct a linear interpolant basis for (``self``,`J`) in `s`-Popov form. + Construct a linear interpolant basis for (``self``,``J``) in ``s``-Popov form. INPUT: - ``J`` -- square matrix for Krylov construction. - ``var_name`` -- variable name for the returned polynomial matrix - ``degree`` -- upper bound on degree of minpoly(`J`), power of - - ``shift`` -- list of integers (optional): controls priority of rows. + - ``shift`` -- list of self.nrows() integers (optional): controls + priority of rows. OUTPUT: - - Matrix whose rows form an interpolation basis. + - A self.ncols() * self.ncols() matrix whose rows form an interpolation + basis. + + TESTS:: + + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: degree = E.ncols() + sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: basis = E.linear_interpolation_basis(J,'x') + sage: basis + [x^2 + 40*x + 82 76 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + sage: basis.is_popov() + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True + + sage: shift = [0,3,6] + sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis + [ x^3 0 0] + [60*x^2 + 72*x + 70 1 0] + [60*x^2 + 72*x + 69 0 1] + sage: basis.is_popov(shifts=shift) + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True + sage: shift = [3,0,2] + sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis + [ 1 26*x^2 + 49*x + 79 0] + [ 0 x^3 0] + [ 0 26*x^2 + 49*x + 78 1] + sage: basis.is_popov(shifts=shift) + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix @@ -19505,19 +19672,19 @@ cdef class Matrix(Matrix1): m = self.nrows() sigma = self.ncols() - + poly_ring = PolynomialRing(self.base_ring(),var_name) - + # calculate krylov profile row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift,output_pairs=True) if len(row_profile) == 0: return matrix.identity(poly_ring,m) - + # (c_k, d_k) = phi^-1 (row_i) #c, d = zip(*(phi_inv(i) for i in row_profile)) c, d = zip(*(row for row in row_profile)) - + # degree_c = 0 or max d[k] such that c[k] = c inv_c = [None]*m degree_c = [0]*m @@ -19525,7 +19692,7 @@ cdef class Matrix(Matrix1): if d[k] >= degree_c[c[k]]: degree_c[c[k]] = d[k] + 1 inv_c[c[k]] = k - + T_absent_indices = [i for i in range(m) if inv_c[i] is None] T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) @@ -19540,13 +19707,11 @@ cdef class Matrix(Matrix1): else: D_rows.append(D_present[idx_p]) idx_p += 1 - + C = pivot.matrix_from_columns(col_profile) - D = matrix(D_rows) - relation = D*C.inverse() - + # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields coeffs_map = [[{} for _ in range(m)] for _ in range(m)] # add identity part of basis @@ -19598,7 +19763,7 @@ cdef class Matrix(Matrix1): basis_rows[row][col] = poly.shift(mindeg) # convert to matrix return matrix(poly_ring,basis_rows) - + # a limited number of access-only properties are provided for matrices @property def T(self): From fa0bf8c31f7b59ba4e3fa04ecd15d9084d9c0c74 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Tue, 29 Jul 2025 18:07:45 +0200 Subject: [PATCH 22/57] remove testing functions from development --- src/sage/matrix/matrix2.pyx | 233 -------------------- src/sage/matrix/matrix_polynomial_dense.pyx | 41 ---- 2 files changed, 274 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 3a82627f472..3d239472321 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19040,237 +19040,6 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def striped_krylov_matrix(self, J, degree, shift=None): - r""" - Return the block matrix of the following form, with rows permuted according to shift. - The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. - - [ ] - [ E ] - [ ] - [-----] - [ ] - [ EJ ] - [ ] - [-----] - [ . ] - [ . ] - [ . ] - [-----] - [ ] - [ EJ^d] - [ ] - - INPUT: - - - ``J`` -- a square matrix of size equal to the number of columns of ``self``. - - ``degree`` -- integer, the maximum exponent for the Krylov matrix. - - ``shift`` -- list of integers (optional), row priority shifts. If ``None``, - defaults to all zero. - - OUTPUT: - - - A matrix with block rows [E, EJ, EJ^2, ..., EJ^d], row-permuted by shift. - - EXAMPLES: - - sage: R. = GF(97)[] - sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: E.striped_krylov_matrix(J,3) - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.striped_krylov_matrix(J,3,[0,3,6]) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.striped_krylov_matrix(J,3,[3,0,2]) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - - """ - from sage.combinat.permutation import Permutation - from sage.matrix.constructor import matrix # for matrix.block - - m = self.nrows() - - # if no shift, this is equivalent to 0s - if shift is None: - shift = [0]*self.nrows() - - # size checks - if len(shift) != m: - raise ValueError(f"The shift should have the same number of elements as the rows of the matrix ({m}), but had {len(shift)}.") - if not J.is_square() or J.nrows() != self.ncols(): - raise ValueError(f"The matrix J should be a square matrix and match the number of columns of self ({self.ncols()}), but is of dimension {J.nrows()} x {J.ncols()}.") - - # define priority and indexing functions - # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shift[c] + d - index = lambda i : (i%m,i//m) - - # deduce permutation by sorting priorities - # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) - priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - - # build all blocks for the striped matrix - blocks = [] - - # for i from 0 to degree (inclusive), add E*J^i - J_i = matrix.identity(self.ncols()) - for i in range(degree+1): - blocks.append([self*J_i]) - if i < degree: - J_i *= J - - # return block matrix permuted according to shift - krylov = matrix.block(blocks,subdivide=False) - krylov.permute_rows(priority_permutation) - return krylov - - def naive_krylov_rank_profile(self, J, degree, shift=None): - r""" - Compute the rank profile (row and column) of the striped Krylov matrix - built from ``self`` and matrix `J`. - - INPUT: - - - ``J`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of J in Krylov matrix. - - ``shift`` -- list of integers (optional): priority row shift. - - OUTPUT: - - - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the striped Krylov matrix `K` corresponding to independent rows - * pivot: the submatrix of `K` given by those rows - * column_profile: list of the first r independent column indices in ``pivot_matrix`` - where r is the rank of `K`. - - EXAMPLES: - - sage: R. = GF(97)[] - sage: E = matrix([[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: E.striped_krylov_matrix(J,3) - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3) - ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[0,3,6]) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3,[0,3,6]) - ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) - ) - sage: E.striped_krylov_matrix(J,3,[3,0,2]) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.naive_krylov_rank_profile(J,3,[3,0,2]) - ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) - ) - """ - from sage.matrix.constructor import matrix - - K = self.striped_krylov_matrix(J,degree,shift) - - # calculate first independent rows of K - row_profile = K.row_rank_profile() - - # construct submatrix - pivot = K.matrix_from_rows(row_profile) - - # calculate first independent columns of pivot - col_profile = pivot.col_rank_profile() - - return row_profile,pivot,col_profile - def krylov_rank_profile(self, J, degree=None, shift=None, output_pairs=False): r""" Compute the rank profile (row and column) of the striped Krylov matrix @@ -20387,8 +20156,6 @@ def _matrix_power_symbolic(A, n): return P * M * Pinv - - class NotFullRankError(ValueError): """ diff --git a/src/sage/matrix/matrix_polynomial_dense.pyx b/src/sage/matrix/matrix_polynomial_dense.pyx index 4e147c2f82e..db6ff8f4ccf 100644 --- a/src/sage/matrix/matrix_polynomial_dense.pyx +++ b/src/sage/matrix/matrix_polynomial_dense.pyx @@ -5337,44 +5337,3 @@ cdef class Matrix_polynomial_dense(Matrix_generic_dense): return False return True - - def expansion(self, degree=None): - r""" - Return the horizontal expansion of the matrix, - where the ith block corresponds to the coefficients for x^i. - - INPUT: - - - ``degree`` -- integer, the degree bound on the matrix; - if ``None``, defaults to degree of matrix. - - OUTPUT: matrix - - EXAMPLES: - sage: R. = GF(97)[] - sage: M = matrix([[x^2+36*x,31*x,0],[3*x+13,x+57,0],[96,96,1]]) - sage: M - [x^2 + 36*x 31*x 0] - [ 3*x + 13 x + 57 0] - [ 96 96 1] - sage: M.expansion() - [ 0 0 0 36 31 0 1 0 0] - [13 57 0 3 1 0 0 0 0] - [96 96 1 0 0 0 0 0 0] - sage: M.expansion(1) - [ 0 0 0 36 31 0] - [13 57 0 3 1 0] - [96 96 1 0 0 0] - sage: M.expansion(3) - [ 0 0 0 36 31 0 1 0 0 0 0 0] - [13 57 0 3 1 0 0 0 0 0 0 0] - [96 96 1 0 0 0 0 0 0 0 0 0] - """ - from sage.matrix.constructor import matrix # for matrix.block - - if degree is None: - degree = self.degree() - - # compute each coefficient matrix - coeff_matrices = [[self.coefficient_matrix(i) for i in range(degree+1)]] - return matrix.block(coeff_matrices,subdivide=False) \ No newline at end of file From e55bb367963452232f0dc80f2ffaa7a7b0b7ab14 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 31 Jul 2025 18:43:45 +0200 Subject: [PATCH 23/57] updates --- src/sage/matrix/matrix2.pyx | 442 +++++++++--------- .../matrix/matrix_modn_dense_template.pxi | 8 +- 2 files changed, 221 insertions(+), 229 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 3d239472321..adbd9117485 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -1065,22 +1065,6 @@ cdef class Matrix(Matrix1): v = self.transpose().pivots() self.cache('pivot_rows', v) return v - - def row_rank_profile(self): - """ - Alias for pivot_rows(self). Return the pivot row positions for this matrix, which are a topmost - subset of the rows that span the row space and are linearly - independent. - """ - return self.pivot_rows() - - def col_rank_profile(self): - """ - Alias for pivots(self). Return the pivot column positions for this matrix, which are a leftmost - subset of the columns that span the column space and are linearly - independent. - """ - return self.pivots() def _solve_right_general(self, B, check=True): r""" @@ -19053,155 +19037,155 @@ cdef class Matrix(Matrix1): OUTPUT: - - A tuple (row_profile, pivot_matrix, column_profile): - * ``row_profile``: list of the first r row indices in the striped + A tuple (row_profile, pivot_matrix, column_profile): + - ``row_profile``: list of the first r row indices in the striped Krylov matrix ``K`` corresponding to independent rows. If ``output_pairs`` is True, then the output is the list of pairs (c_i, d_i) where c_i is the corresponding row in the original matrix and d_i the corresponding power of ``J``. - * ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - * ``column_profile``: the first r independent column indices in + - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + - ``column_profile``: the first r independent column indices in ``pivot_matrix`` where r is the rank of `K`. TESTS:: - sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: degree = 3 - sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) - sage: striped_krylov_matrix - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J) - ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) - ) - sage: E.krylov_rank_profile(J,output_pairs=True) - ( - [27 49 29] - [50 58 0] - ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) - ) + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: degree = 3 + sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.krylov_rank_profile(J,output_pairs=True) + ( + [27 49 29] + [50 58 0] + ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) + ) - sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] - sage: rows - [(1, 0, 0), - (2, 1, 0), - (3, 2, 0), - (4, 0, 1), - (5, 1, 1), - (6, 2, 1), - (7, 0, 2), - (8, 1, 2), - (9, 2, 2), - (10, 0, 3), - (11, 1, 3), - (12, 2, 3)] - sage: shift = [0,3,6] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) - sage: rows - [(1, 0, 0), - (4, 0, 1), - (7, 0, 2), - (10, 0, 3), - (2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (11, 1, 3), - (3, 2, 0), - (6, 2, 1), - (9, 2, 2), - (12, 2, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_rank_profile(J,shift=shift) - ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) - ) - sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) - ( - [27 49 29] - [ 0 27 49] - ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) - ) + sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] + sage: rows + [(1, 0, 0), + (2, 1, 0), + (3, 2, 0), + (4, 0, 1), + (5, 1, 1), + (6, 2, 1), + (7, 0, 2), + (8, 1, 2), + (9, 2, 2), + (10, 0, 3), + (11, 1, 3), + (12, 2, 3)] + sage: shift = [0,3,6] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(1, 0, 0), + (4, 0, 1), + (7, 0, 2), + (10, 0, 3), + (2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (11, 1, 3), + (3, 2, 0), + (6, 2, 1), + (9, 2, 2), + (12, 2, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_rank_profile(J,shift=shift) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) + ( + [27 49 29] + [ 0 27 49] + ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) + ) - sage: shift = [3,0,2] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) - sage: rows - [(2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (3, 2, 0), - (1, 0, 0), - (11, 1, 3), - (6, 2, 1), - (4, 0, 1), - (9, 2, 2), - (7, 0, 2), - (12, 2, 3), - (10, 0, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_rank_profile(J,shift=shift) - ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) - ) - sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) - ( - [50 58 0] - [ 0 50 58] - ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) - ) + sage: shift = [3,0,2] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (3, 2, 0), + (1, 0, 0), + (11, 1, 3), + (6, 2, 1), + (4, 0, 1), + (9, 2, 2), + (7, 0, 2), + (12, 2, 3), + (10, 0, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_rank_profile(J,shift=shift) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) + ( + [50 58 0] + [ 0 50 58] + ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) + ) """ from sage.matrix.constructor import matrix import math @@ -19323,7 +19307,19 @@ cdef class Matrix(Matrix1): def linear_interpolation_basis(self, J, var_name, degree=None, shift=None): r""" - Construct a linear interpolant basis for (``self``,``J``) in ``s``-Popov form. + Return a shifted minimal linear interpolation basis for + (``self``,``J``) in ``s``-Popov form with respect to a shift ``s``. + + Given a K[x]-module V defined by multiplication matrix J, i.e. elements + are vectors of length n in K, scalars are polynomials in K[x], and + scalar multiplication is defined by p v := v p(J), ``self`` represents + ``self.nrows()`` elements e_1, ..., e_m in V: + + A linear interpolation basis is a set of tuples of length + ``self.nrows()`` in K[x] that form a basis for solutions to the + equation ``p_1 e_1 + ... + p_n e_n = 0``. + + See https://arxiv.org/abs/1512.03503 INPUT: @@ -19340,75 +19336,75 @@ cdef class Matrix(Matrix1): TESTS:: - sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J - [0 1 0] - [0 0 1] - [0 0 0] - sage: degree = E.ncols() - sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) - sage: striped_krylov_matrix - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: basis = E.linear_interpolation_basis(J,'x') - sage: basis - [x^2 + 40*x + 82 76 0] - [ 3*x + 13 x + 57 0] - [ 96 96 1] - sage: basis.is_popov() - True - sage: basis.degree() <= E.ncols() - True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) - True - sage: len(E.krylov_rank_profile(J)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) - True - - sage: shift = [0,3,6] - sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) - sage: basis - [ x^3 0 0] - [60*x^2 + 72*x + 70 1 0] - [60*x^2 + 72*x + 69 0 1] - sage: basis.is_popov(shifts=shift) - True - sage: basis.degree() <= E.ncols() - True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) - True - sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) - True - - sage: shift = [3,0,2] - sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) - sage: basis - [ 1 26*x^2 + 49*x + 79 0] - [ 0 x^3 0] - [ 0 26*x^2 + 49*x + 78 1] - sage: basis.is_popov(shifts=shift) - True - sage: basis.degree() <= E.ncols() - True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) - True - sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) - True + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: J + [0 1 0] + [0 0 1] + [0 0 0] + sage: degree = E.ncols() + sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: basis = E.linear_interpolation_basis(J,'x') + sage: basis + [x^2 + 40*x + 82 76 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + sage: basis.is_popov() + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True + + sage: shift = [0,3,6] + sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis + [ x^3 0 0] + [60*x^2 + 72*x + 70 1 0] + [60*x^2 + 72*x + 69 0 1] + sage: basis.is_popov(shifts=shift) + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True + + sage: shift = [3,0,2] + sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis + [ 1 26*x^2 + 49*x + 79 0] + [ 0 x^3 0] + [ 0 26*x^2 + 49*x + 78 1] + sage: basis.is_popov(shifts=shift) + True + sage: basis.degree() <= E.ncols() + True + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + True + sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix @@ -19450,8 +19446,6 @@ cdef class Matrix(Matrix1): if len(row_profile) == 0: return matrix.identity(poly_ring,m) - # (c_k, d_k) = phi^-1 (row_i) - #c, d = zip(*(phi_inv(i) for i in row_profile)) c, d = zip(*(row for row in row_profile)) # degree_c = 0 or max d[k] such that c[k] = c diff --git a/src/sage/matrix/matrix_modn_dense_template.pxi b/src/sage/matrix/matrix_modn_dense_template.pxi index 4655b3be180..2a304da51d8 100644 --- a/src/sage/matrix/matrix_modn_dense_template.pxi +++ b/src/sage/matrix/matrix_modn_dense_template.pxi @@ -204,13 +204,12 @@ cdef inline linbox_echelonize(celement modulus, celement* entries, Py_ssize_t nr applyP(F[0], FflasRight, FflasNoTrans, nrows, 0, r, entries, ncols, Q) - cdef list column_pivots = [int(Q[i]) for i in range(r)] - cdef list row_pivots = sorted([int(P[i]) for i in range(r)]) + cdef list pivots = [int(Q[i]) for i in range(r)] sig_free(P) sig_free(Q) del F - return r, column_pivots, row_pivots + return r, pivots cdef inline linbox_echelonize_efd(celement modulus, celement* entries, Py_ssize_t nrows, Py_ssize_t ncols): # See trac #13878: This is to avoid sending invalid data to linbox, @@ -1789,9 +1788,8 @@ cdef class Matrix_modn_dense_template(Matrix_dense): r, pivots = linbox_echelonize_efd(self.p, self._entries, self._nrows, self._ncols) else: - r, pivots, row_pivots = linbox_echelonize(self.p, self._entries, + r, pivots = linbox_echelonize(self.p, self._entries, self._nrows, self._ncols) - self.cache('pivot_rows',tuple(row_pivots)) verbose('done with echelonize', t) self.cache('in_echelon_form', True) self.cache('rank', r) From 4f4d6e811601d8209b4e5b96c2a8d31cb5ccf973 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 31 Jul 2025 22:31:14 +0200 Subject: [PATCH 24/57] changes --- src/sage/matrix/matrix2.pyx | 6 +++--- src/sage/matrix/matrix_modn_dense_template.pxi | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 2e580141736..53aaa80572c 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19301,7 +19301,7 @@ cdef class Matrix(Matrix1): self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) self_permuted = self.with_permuted_rows(self_permutation) - row_profile_self_permuted = self_permuted.row_rank_profile() + row_profile_self_permuted = self_permuted.pivot_rows() row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] exhausted = matrix.zero(self.base_ring(), 0, sigma) @@ -19341,7 +19341,7 @@ cdef class Matrix(Matrix1): # sort rows of M, find profile, translate to k (indices of full krylov matrix) M.permute_rows(k_perm) - row_profile_M = M.row_rank_profile() + row_profile_M = M.pivot_rows() r = len(row_profile_M) if r == sigma and l < math.ceil(math.log(degree,2)): @@ -19373,7 +19373,7 @@ cdef class Matrix(Matrix1): M.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] - col_profile = M.col_rank_profile() + col_profile = M.pivots() if output_pairs: return tuple(row_profile_self), M, col_profile diff --git a/src/sage/matrix/matrix_modn_dense_template.pxi b/src/sage/matrix/matrix_modn_dense_template.pxi index 2a304da51d8..8e9e7bf0f89 100644 --- a/src/sage/matrix/matrix_modn_dense_template.pxi +++ b/src/sage/matrix/matrix_modn_dense_template.pxi @@ -176,7 +176,7 @@ cdef inline linbox_echelonize(celement modulus, celement* entries, Py_ssize_t nr Return the reduced row echelon form of this matrix. """ if linbox_is_zero(modulus, entries, nrows, ncols): - return 0, [], [] + return 0, [] cdef Py_ssize_t i, j cdef ModField *F = new ModField(modulus) From 1a89eee290791f56c9fc27ba36f10df821cce87e Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 4 Aug 2025 16:14:55 +0200 Subject: [PATCH 25/57] readd naive methods, changes from review --- src/sage/matrix/matrix2.pyx | 517 ++++++++++++++++++++++++++++++------ 1 file changed, 432 insertions(+), 85 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 53aaa80572c..d59eefea214 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19103,31 +19103,378 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def krylov_rank_profile(self, J, degree=None, shift=None, output_pairs=False): + def striped_krylov_matrix(self, M, shift=None, degree=None): + r""" + Return the block matrix of the following form, with rows permuted according to shift. + The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. + + [ ] + [ E ] + [ ] + [-----] + [ ] + [ EM ] + [ ] + [-----] + [ . ] + [ . ] + [ . ] + [-----] + [ ] + [ EM^d] + [ ] + + INPUT: + + - ``M`` -- a square matrix of size equal to the number of columns of ``self``. + - ``degree`` -- integer, the maximum exponent for the Krylov matrix. + - ``shift`` -- list of integers (optional), row priority shifts. If ``None``, + defaults to all zero. + + OUTPUT: + + - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by shift. + + EXAMPLES:: + + sage: R. = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.striped_krylov_matrix(M) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.striped_krylov_matrix(M,[0,3,6]) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.striped_krylov_matrix(M,[3,0,2]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + + """ + from sage.combinat.permutation import Permutation + import math + from sage.matrix.constructor import matrix # for matrix.block + + if not isinstance(M, Matrix): + raise TypeError("striped_krylov_matrix: M is not a matrix") + if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + raise ValueError("striped_krylov_matrix: matrix M does not have correct dimensions.") + if M.base_ring() != self.base_ring(): + raise ValueError("striped_krylov_matrix: matrix M does not have same base ring as E.") + + if degree is None: + degree = self.ncols() + if not isinstance(degree, (int, sage.rings.integer.Integer)): + raise TypeError("striped_krylov_matrix: degree is not an integer.") + if degree < 0: + raise ValueError("striped_krylov_matrix: degree bound cannot be negative.") + + if shift is None or shift == 0: + shift = (ZZ**self.nrows()).zero() + if isinstance(shift, (list, tuple)): + shift = (ZZ**self.nrows())(shift) + if shift not in ZZ**self.nrows(): + raise ValueError(f"striped_krylov_matrix: shift is not an integer vector of length {self.nrows()}.") + + m = self.nrows() + + # define priority and indexing functions + # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + + # deduce permutation by sorting priorities + # rows should be ordered by ascending priority, then by associated row number in E (i%m) + priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) + + # build all blocks for the striped matrix + blocks = [self] + + # for i from 0 to degree (inclusive), add E*M^i + M_i = None + for i in range(math.ceil(math.log(degree + 1, 2))): + if M_i is None: + M_i = M + else: + M_i = M_i * M_i + blocks = [matrix.block([[blocks[0]],[blocks[1]]],subdivide=False)] + blocks.append(blocks[0] * M_i) + + if (degree + 1) & degree != 0: + blocks[1] = blocks[1].matrix_from_rows(range((degree + 1) * m - blocks[1].nrows())) + + # return block matrix permuted according to shift + krylov = matrix.block([[blocks[0]],[blocks[1]]],subdivide=False) + krylov.permute_rows(priority_permutation) + return krylov + + def naive_krylov_profile(self, M, shift=None, degree=None, output_pairs=True): r""" Compute the rank profile (row and column) of the striped Krylov matrix - built from ``self`` and matrix `J`. + built from ``self`` and matrix ``M``. INPUT: - - ``J`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of J in Krylov matrix. + - ``M`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. - ``shift`` -- list of integers (optional): priority row shift. OUTPUT: + - A tuple (row_profile, pivot_matrix, column_profile): + * row_profile: list of the first r row indices in the striped Krylov matrix ``K`` corresponding to independent rows + * pivot: the submatrix of ``K`` given by those rows + * column_profile: list of the first r independent column indices in ``pivot_matrix`` + where r is the rank of ``K``. + + TESTS:: + + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M + [0 1 0] + [0 0 1] + [0 0 0] + sage: degree = 3 + sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_profile(M, output_pairs=False) + ( + [27 49 29] + [50 58 0] + (0, 1, 3), [ 0 27 49], (0, 1, 2) + ) + sage: E.krylov_profile(M) + ( + [27 49 29] + [50 58 0] + ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) + ) + + sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] + sage: rows + [(1, 0, 0), + (2, 1, 0), + (3, 2, 0), + (4, 0, 1), + (5, 1, 1), + (6, 2, 1), + (7, 0, 2), + (8, 1, 2), + (9, 2, 2), + (10, 0, 3), + (11, 1, 3), + (12, 2, 3)] + sage: shift = [0,3,6] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(1, 0, 0), + (4, 0, 1), + (7, 0, 2), + (10, 0, 3), + (2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (11, 1, 3), + (3, 2, 0), + (6, 2, 1), + (9, 2, 2), + (12, 2, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_profile(M, shift=shift, output_pairs=False) + ( + [27 49 29] + [ 0 27 49] + (0, 1, 2), [ 0 0 27], (0, 1, 2) + ) + sage: E.krylov_profile(M, shift=shift) + ( + [27 49 29] + [ 0 27 49] + ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) + ) + + sage: shift = [3,0,2] + sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: rows + [(2, 1, 0), + (5, 1, 1), + (8, 1, 2), + (3, 2, 0), + (1, 0, 0), + (11, 1, 3), + (6, 2, 1), + (4, 0, 1), + (9, 2, 2), + (7, 0, 2), + (12, 2, 3), + (10, 0, 3)] + sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_profile(M,shift=shift,output_pairs=False) + ( + [50 58 0] + [ 0 50 58] + (0, 1, 2), [ 0 0 50], (0, 1, 2) + ) + sage: E.krylov_profile(M,shift=shift) + ( + [50 58 0] + [ 0 50 58] + ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) + ) + """ + from sage.matrix.constructor import matrix + + if not isinstance(M, Matrix): + raise TypeError("naive_krylov_profile: M is not a matrix") + if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + raise ValueError("naive_krylov_profile: matrix M does not have correct dimensions.") + if M.base_ring() != self.base_ring(): + raise ValueError("naive_krylov_profile: matrix M does not have same base ring as E.") + + if degree is None: + degree = self.ncols() + if not isinstance(degree, (int, sage.rings.integer.Integer)): + raise TypeError("naive_krylov_profile: degree is not an integer.") + if degree < 0: + raise ValueError("naive_krylov_profile: degree bound cannot be negative.") + + if shift is None or shift == 0: + shift = (ZZ**self.nrows()).zero() + if isinstance(shift, (list, tuple)): + shift = (ZZ**self.nrows())(shift) + if shift not in ZZ**self.nrows(): + raise ValueError(f"naive_krylov_profile: shift is not an integer vector of length {self.nrows()}.") + + m = self.nrows() + + K = self.striped_krylov_matrix(M,shift,degree) + + # define priority and indexing functions + # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + + # deduce permutation by sorting priorities + # rows should be ordered by ascending priority, then by associated row number in E (i%m) + priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(degree+1))]) + + # calculate first independent rows of K + row_profile = K.pivot_rows() + + # construct submatrix + pivot = K.matrix_from_rows(row_profile) + + # calculate first independent columns of pivot + col_profile = pivot.pivots() + + row_profile = tuple([priority_pairs[i][1] for i in row_profile]) + + return row_profile,pivot,col_profile + + def krylov_profile(self, M, shift=None, degree=None, output_pairs=True): + r""" + Compute the rank profile (row and column) of the block Krylov matrix + built from ``self`` and matrix ``M``. + + INPUT: + + - ``M`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. + - ``shift`` -- list of ``self.nrows()`` integers (optional): priority + row shift. + + OUTPUT: + A tuple (row_profile, pivot_matrix, column_profile): - - ``row_profile``: list of the first r row indices in the striped - Krylov matrix ``K`` corresponding to - independent rows. If ``output_pairs`` is True, - then the output is the list of pairs (c_i, d_i) - where c_i is the corresponding row in the - original matrix and d_i the corresponding power - of ``J``. - - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - - ``column_profile``: the first r independent column indices in - ``pivot_matrix`` - where r is the rank of `K`. + - ``row_profile``: list of the first r row indices in the striped + Krylov matrix ``K`` corresponding to independent rows. If + ``output_pairs`` is True, then the output is the list of pairs + (c_i, d_i) where c_i is the corresponding row in the original matrix + and d_i the corresponding power of ``M``. + - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + - ``column_profile``: the first r independent column indices in + ``pivot_matrix`` where r is the rank of `K`. TESTS:: @@ -19137,13 +19484,13 @@ cdef class Matrix(Matrix1): [27 49 29] [50 58 0] [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M [0 1 0] [0 0 1] [0 0 0] sage: degree = 3 - sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) sage: striped_krylov_matrix [27 49 29] [50 58 0] @@ -19157,13 +19504,13 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_rank_profile(J) + sage: E.krylov_profile(M, output_pairs=False) ( [27 49 29] [50 58 0] (0, 1, 3), [ 0 27 49], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,output_pairs=True) + sage: E.krylov_profile(M) ( [27 49 29] [50 58 0] @@ -19212,13 +19559,13 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_rank_profile(J,shift=shift) + sage: E.krylov_profile(M, shift=shift, output_pairs=False) ( [27 49 29] [ 0 27 49] (0, 1, 2), [ 0 0 27], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) + sage: E.krylov_profile(M, shift=shift) ( [27 49 29] [ 0 27 49] @@ -19253,13 +19600,13 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_rank_profile(J,shift=shift) + sage: E.krylov_profile(M,shift=shift,output_pairs=False) ( [50 58 0] [ 0 50 58] (0, 1, 2), [ 0 0 50], (0, 1, 2) ) - sage: E.krylov_rank_profile(J,shift=shift,output_pairs=True) + sage: E.krylov_profile(M,shift=shift) ( [50 58 0] [ 0 50 58] @@ -19270,26 +19617,26 @@ cdef class Matrix(Matrix1): import math from sage.combinat.permutation import Permutation - if not isinstance(J, Matrix): - raise TypeError("krylov_rank_profile: J is not a matrix") - if J.nrows() != self.ncols() or J.ncols() != self.ncols(): - raise ValueError("krylov_rank_profile: matrix J does not have correct dimensions.") - if J.base_ring() != self.base_ring(): - raise ValueError("krylov_rank_profile: matrix J does not have same base ring as E.") + if not isinstance(M, Matrix): + raise TypeError("krylov_profile: M is not a matrix") + if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + raise ValueError("krylov_profile: matrix M does not have correct dimensions.") + if M.base_ring() != self.base_ring(): + raise ValueError("krylov_profile: matrix M does not have same base ring as E.") if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_rank_profile: degree is not an integer.") + raise TypeError("krylov_profile: degree is not an integer.") if degree < 0: - raise ValueError("krylov_rank_profile: degree bound cannot be negative.") + raise ValueError("krylov_profile: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_rank_profile: shift is not a Z-vector of length {self.nrows()}.") + raise ValueError(f"krylov_profile: shift is not a Z-vector of length {self.nrows()}.") m = self.nrows() sigma = self.ncols() @@ -19313,11 +19660,11 @@ cdef class Matrix(Matrix1): if r == 0: return (),self.matrix_from_rows([]),() - M = self_permuted.matrix_from_rows(row_profile_self_permuted) + R = self_permuted.matrix_from_rows(row_profile_self_permuted) - J_L = None + M_L = None - for l in range(math.ceil(math.log(degree,2)) + 1): + for l in range(math.ceil(math.log(degree + 1, 2))): L = pow(2,l) # adding 2^l to each degree row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] @@ -19331,67 +19678,67 @@ cdef class Matrix(Matrix1): k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) # fast calculation of rows formed by indices in k - if J_L is None: - J_L = J + if M_L is None: + M_L = M else: - J_L = J_L * J_L + M_L = M_L * M_L - M = matrix.block([[exhausted],[M],[M*J_L]],subdivide=False) + R = matrix.block([[exhausted],[R],[R*M_L]],subdivide=False) - # sort rows of M, find profile, translate to k (indices of full krylov matrix) - M.permute_rows(k_perm) + # sort rows of R, find profile, translate to k (indices of full krylov matrix) + R.permute_rows(k_perm) - row_profile_M = M.pivot_rows() - r = len(row_profile_M) + row_profile_R = R.pivot_rows() + r = len(row_profile_R) if r == sigma and l < math.ceil(math.log(degree,2)): - tail = list(range(row_profile_M[-1]+1,M.nrows())) + tail = list(range(row_profile_R[-1]+1,R.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) - xmi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] in excluded_rows] - imi = [i for i in row_profile_M if k[k_perm(i+1)-1][0] not in excluded_rows] + xmi = [i for i in row_profile_R if k[k_perm(i+1)-1][0] in excluded_rows] + imi = [i for i in row_profile_R if k[k_perm(i+1)-1][0] not in excluded_rows] row_profile_exhausted = [k[k_perm(i+1)-1] for i in xmi] row_profile_self = [k[k_perm(i+1)-1] for i in imi] - exhausted = M.matrix_from_rows(xmi) - M = M.matrix_from_rows(imi) + exhausted = R.matrix_from_rows(xmi) + R = R.matrix_from_rows(imi) else: if l == math.ceil(math.log(degree,2)): row_profile_exhausted = [] exhausted = matrix.zero(self.base_ring(), 0, sigma) - row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_M] - # calculate new M for return value or next loop - M = M.matrix_from_rows(row_profile_M) - if M.nrows() == 0: - M = exhausted + row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_R] + # calculate new R for return value or next loop + R = R.matrix_from_rows(row_profile_R) + if R.nrows() == 0: + R = exhausted row_profile_self = row_profile_exhausted elif exhausted.nrows() != 0: k = row_profile_exhausted + row_profile_self - M = exhausted.stack(M) + R = exhausted.stack(R) k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) - M.permute_rows(k_perm) + R.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] - col_profile = M.pivots() + col_profile = R.pivots() if output_pairs: - return tuple(row_profile_self), M, col_profile + return tuple(row_profile_self), R, col_profile # convert c,d to actual position in striped Krylov matrix phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) row_profile_K = [phi(*row) for row in row_profile_self] - return tuple(row_profile_K), M, col_profile + return tuple(row_profile_K), R, col_profile - def linear_interpolation_basis(self, J, var_name, degree=None, shift=None): + def linear_interpolation_basis(self, M, var_name, degree=None, shift=None): r""" Return a shifted minimal linear interpolation basis for - (``self``,``J``) in ``s``-Popov form with respect to a shift ``s``. + (``self``,``M``) in ``s``-Popov form with respect to a shift ``s``. - Given a K[x]-module V defined by multiplication matrix J, i.e. elements + Given a K[x]-module V defined by multiplication matrix ``M``, i.e. elements are vectors of length n in K, scalars are polynomials in K[x], and - scalar multiplication is defined by p v := v p(J), ``self`` represents + scalar multiplication is defined by p v := v p(``M``), ``self`` represents ``self.nrows()`` elements e_1, ..., e_m in V: A linear interpolation basis is a set of tuples of length @@ -19402,9 +19749,9 @@ cdef class Matrix(Matrix1): INPUT: - - ``J`` -- square matrix for Krylov construction. + - ``M`` -- square matrix for Krylov construction. - ``var_name`` -- variable name for the returned polynomial matrix - - ``degree`` -- upper bound on degree of minpoly(`J`), power of + - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a suitable upper bound of ``self.ncols()`` is default. - ``shift`` -- list of self.nrows() integers (optional): controls priority of rows. @@ -19421,13 +19768,13 @@ cdef class Matrix(Matrix1): [27 49 29] [50 58 0] [77 10 29] - sage: J = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: J + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M [0 1 0] [0 0 1] [0 0 0] sage: degree = E.ncols() - sage: striped_krylov_matrix = matrix.block([[E*J^i] for i in range(degree+1)],subdivide=False) + sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) sage: striped_krylov_matrix [27 49 29] [50 58 0] @@ -19441,7 +19788,7 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: basis = E.linear_interpolation_basis(J,'x') + sage: basis = E.linear_interpolation_basis(M,'x') sage: basis [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] @@ -19452,11 +19799,11 @@ cdef class Matrix(Matrix1): True sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_rank_profile(J)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_profile(M)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shift = [0,3,6] - sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis = E.linear_interpolation_basis(M,'x',shift=shift) sage: basis [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] @@ -19467,11 +19814,11 @@ cdef class Matrix(Matrix1): True sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_profile(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shift = [3,0,2] - sage: basis = E.linear_interpolation_basis(J,'x',shift=shift) + sage: basis = E.linear_interpolation_basis(M,'x',shift=shift) sage: basis [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] @@ -19482,7 +19829,7 @@ cdef class Matrix(Matrix1): True sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_rank_profile(J,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_profile(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True """ from sage.combinat.permutation import Permutation @@ -19490,12 +19837,12 @@ cdef class Matrix(Matrix1): from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing # INPUT VALIDATION - if not isinstance(J, Matrix): - raise TypeError("krylov_rank_profile: J is not a matrix") - if J.nrows() != self.ncols() or J.ncols() != self.ncols(): - raise ValueError("krylov_rank_profile: matrix J does not have correct dimensions.") - if J.base_ring() != self.base_ring(): - raise ValueError("krylov_rank_profile: matrix J does not have same base ring as E.") + if not isinstance(M, Matrix): + raise TypeError("linear_interpolation_basis: M is not a matrix") + if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + raise ValueError("linear_interpolation_basis: matrix M does not have correct dimensions.") + if M.base_ring() != self.base_ring(): + raise ValueError("linear_interpolation_basis: matrix M does not have same base ring as E.") if not isinstance(var_name, str): raise TypeError("var_name is not a string") @@ -19503,16 +19850,16 @@ cdef class Matrix(Matrix1): if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_rank_profile: degree is not an integer.") + raise TypeError("linear_interpolation_basis: degree is not an integer.") if degree < 0: - raise ValueError("krylov_rank_profile: degree bound cannot be negative.") + raise ValueError("linear_interpolation_basis: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_rank_profile: shift is not a Z-vector of length {self.nrows()}.") + raise ValueError(f"linear_interpolation_basis: shift is not a Z-vector of length {self.nrows()}.") m = self.nrows() sigma = self.ncols() @@ -19520,7 +19867,7 @@ cdef class Matrix(Matrix1): poly_ring = PolynomialRing(self.base_ring(),var_name) # calculate krylov profile - row_profile, pivot, col_profile = self.krylov_rank_profile(J,degree,shift,output_pairs=True) + row_profile, pivot, col_profile = self.krylov_profile(M, shift, degree) if len(row_profile) == 0: return matrix.identity(poly_ring,m) @@ -19538,7 +19885,7 @@ cdef class Matrix(Matrix1): T_absent_indices = [i for i in range(m) if inv_c[i] is None] T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) - D_present = (pivot.matrix_from_rows(T_present_indices)*J).matrix_from_columns(col_profile) + D_present = (pivot.matrix_from_rows(T_present_indices)*M).matrix_from_columns(col_profile) D_rows = [] idx_p = 0 idx_a = 0 From 6b0a721a5f116a38eeefbfa19cedc5d3fcab43d4 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 6 Aug 2025 16:51:00 +0200 Subject: [PATCH 26/57] implement coefficient output, various doc changes --- src/sage/matrix/matrix2.pyx | 354 ++++++++++++++++++++++-------------- 1 file changed, 215 insertions(+), 139 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index b5105422411..44965f81c4d 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19111,9 +19111,10 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def striped_krylov_matrix(self, M, shift=None, degree=None): + def krylov_matrix(self, M, shift=None, degree=None): r""" - Return the block matrix of the following form, with rows permuted according to shift. + Return the block matrix built from the rows of ``self`` and the matrix ``M``, with rows ordered according to a priority defined by ``shift``. See [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912] and [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]." + The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. [ ] @@ -19136,12 +19137,13 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - ``degree`` -- integer, the maximum exponent for the Krylov matrix. - - ``shift`` -- list of integers (optional), row priority shifts. If ``None``, - defaults to all zero. + - ``shift`` -- list of ``self.nrows()`` integers (optional), row priority + shifts. If ``None``, defaults to all zeroes. OUTPUT: - - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by shift. + - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by + shift. EXAMPLES:: @@ -19156,7 +19158,7 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: E.striped_krylov_matrix(M) + sage: E.krylov_matrix(M) [27 49 29] [50 58 0] [77 10 29] @@ -19169,7 +19171,7 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.striped_krylov_matrix(M,[0,3,6]) + sage: E.krylov_matrix(M,[0,3,6]) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19182,7 +19184,7 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.striped_krylov_matrix(M,[3,0,2]) + sage: E.krylov_matrix(M,[3,0,2]) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19202,25 +19204,25 @@ cdef class Matrix(Matrix1): from sage.matrix.constructor import matrix # for matrix.block if not isinstance(M, Matrix): - raise TypeError("striped_krylov_matrix: M is not a matrix") + raise TypeError("krylov_matrix: M is not a matrix") if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("striped_krylov_matrix: matrix M does not have correct dimensions.") + raise ValueError("krylov_matrix: matrix M does not have correct dimensions.") if M.base_ring() != self.base_ring(): - raise ValueError("striped_krylov_matrix: matrix M does not have same base ring as E.") + raise ValueError("krylov_matrix: matrix M does not have same base ring as E.") if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("striped_krylov_matrix: degree is not an integer.") + raise TypeError("krylov_matrix: degree is not an integer.") if degree < 0: - raise ValueError("striped_krylov_matrix: degree bound cannot be negative.") + raise ValueError("krylov_matrix: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"striped_krylov_matrix: shift is not an integer vector of length {self.nrows()}.") + raise ValueError(f"krylov_matrix: shift is not an integer vector of length {self.nrows()}.") m = self.nrows() @@ -19234,10 +19236,10 @@ cdef class Matrix(Matrix1): priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - # build all blocks for the striped matrix + # store 2 blocks for the Krylov matrix blocks = [self] - # for i from 0 to degree (inclusive), add E*M^i + # for i from 0 to degree (inclusive), merge existing blocks, add E*M^i M_i = None for i in range(math.ceil(math.log(degree + 1, 2))): if M_i is None: @@ -19255,21 +19257,21 @@ cdef class Matrix(Matrix1): krylov.permute_rows(priority_permutation) return krylov - def naive_krylov_profile(self, M, shift=None, degree=None, output_pairs=True): + def _naive_krylov_basis(self, M, shift=None, degree=None, output_pairs=True): r""" - Compute the rank profile (row and column) of the striped Krylov matrix + Compute the rank profile (row and column) of the Krylov matrix built from ``self`` and matrix ``M``. INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. - - ``shift`` -- list of integers (optional): priority row shift. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. + - ``shift`` -- list of ``self.nrows()`` integers (optional): priority row shift. If ``None``, defaults to all zeroes. OUTPUT: - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the striped Krylov matrix ``K`` corresponding to independent rows + * row_profile: list of the first r row indices in the Krylov matrix ``K`` corresponding to independent rows * pivot: the submatrix of ``K`` given by those rows * column_profile: list of the first r independent column indices in ``pivot_matrix`` where r is the rank of ``K``. @@ -19288,8 +19290,8 @@ cdef class Matrix(Matrix1): [0 0 1] [0 0 0] sage: degree = 3 - sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) - sage: striped_krylov_matrix + sage: K = E.krylov_matrix(M) + sage: K [27 49 29] [50 58 0] [77 10 29] @@ -19302,13 +19304,13 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_profile(M, output_pairs=False) + sage: E.krylov_basis(M, output_pairs=False) ( [27 49 29] [50 58 0] (0, 1, 3), [ 0 27 49], (0, 1, 2) ) - sage: E.krylov_profile(M) + sage: E.krylov_basis(M) ( [27 49 29] [50 58 0] @@ -19344,7 +19346,7 @@ cdef class Matrix(Matrix1): (6, 2, 1), (9, 2, 2), (12, 2, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + sage: E.krylov_matrix(M,shift=shift) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19357,13 +19359,13 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_profile(M, shift=shift, output_pairs=False) + sage: E.krylov_basis(M, shift=shift, output_pairs=False) ( [27 49 29] [ 0 27 49] (0, 1, 2), [ 0 0 27], (0, 1, 2) ) - sage: E.krylov_profile(M, shift=shift) + sage: E.krylov_basis(M,shift=shift) ( [27 49 29] [ 0 27 49] @@ -19385,7 +19387,7 @@ cdef class Matrix(Matrix1): (7, 0, 2), (12, 2, 3), (10, 0, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + sage: E.krylov_matrix(M,shift=shift) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19398,13 +19400,13 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_profile(M,shift=shift,output_pairs=False) + sage: E.krylov_basis(M,shift=shift,output_pairs=False) ( [50 58 0] [ 0 50 58] (0, 1, 2), [ 0 0 50], (0, 1, 2) ) - sage: E.krylov_profile(M,shift=shift) + sage: E.krylov_basis(M,shift=shift) ( [50 58 0] [ 0 50 58] @@ -19414,38 +19416,29 @@ cdef class Matrix(Matrix1): from sage.matrix.constructor import matrix if not isinstance(M, Matrix): - raise TypeError("naive_krylov_profile: M is not a matrix") + raise TypeError("naive_krylov_basis: M is not a matrix") if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("naive_krylov_profile: matrix M does not have correct dimensions.") + raise ValueError("naive_krylov_basis: matrix M does not have correct dimensions.") if M.base_ring() != self.base_ring(): - raise ValueError("naive_krylov_profile: matrix M does not have same base ring as E.") + raise ValueError("naive_krylov_basis: matrix M does not have same base ring as E.") if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("naive_krylov_profile: degree is not an integer.") + raise TypeError("naive_krylov_basis: degree is not an integer.") if degree < 0: - raise ValueError("naive_krylov_profile: degree bound cannot be negative.") + raise ValueError("naive_krylov_basis: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"naive_krylov_profile: shift is not an integer vector of length {self.nrows()}.") + raise ValueError(f"naive_krylov_basis: shift is not an integer vector of length {self.nrows()}.") m = self.nrows() - K = self.striped_krylov_matrix(M,shift,degree) - - # define priority and indexing functions - # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shift[c] + d - index = lambda i : (i%m,i//m) - - # deduce permutation by sorting priorities - # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(degree+1))]) + K = self.krylov_matrix(M,shift,degree) # calculate first independent rows of K row_profile = K.pivot_rows() @@ -19456,11 +19449,22 @@ cdef class Matrix(Matrix1): # calculate first independent columns of pivot col_profile = pivot.pivots() - row_profile = tuple([priority_pairs[i][1] for i in row_profile]) + if output_pairs: + + # define priority and indexing functions + # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] + priority = lambda c,d : shift[c] + d + index = lambda i : (i%m,i//m) + + # deduce permutation by sorting priorities + # rows should be ordered by ascending priority, then by associated row number in E (i%m) + priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(degree+1))]) + + row_profile = tuple([priority_pairs[i][1] for i in row_profile]) return row_profile,pivot,col_profile - def krylov_profile(self, M, shift=None, degree=None, output_pairs=True): + def _elimination_krylov_basis(self, M, shift=None, degree=None, output_pairs=True): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19470,7 +19474,7 @@ cdef class Matrix(Matrix1): - ``M`` -- square matrix used in the Krylov construction. - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. - ``shift`` -- list of ``self.nrows()`` integers (optional): priority - row shift. + row shift. If ``None``, defaults to all zeroes. OUTPUT: @@ -19498,8 +19502,8 @@ cdef class Matrix(Matrix1): [0 0 1] [0 0 0] sage: degree = 3 - sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) - sage: striped_krylov_matrix + sage: K = E.krylov_matrix(M) + sage: K [27 49 29] [50 58 0] [77 10 29] @@ -19512,19 +19516,18 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_profile(M, output_pairs=False) + sage: E.krylov_basis(M,output_pairs=False) ( [27 49 29] [50 58 0] (0, 1, 3), [ 0 27 49], (0, 1, 2) ) - sage: E.krylov_profile(M) + sage: E.krylov_basis(M) ( [27 49 29] [50 58 0] ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) ) - sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] sage: rows [(1, 0, 0), @@ -19554,7 +19557,7 @@ cdef class Matrix(Matrix1): (6, 2, 1), (9, 2, 2), (12, 2, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + sage: E.krylov_matrix(M,shift=shift) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19567,19 +19570,18 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_profile(M, shift=shift, output_pairs=False) + sage: E.krylov_basis(M,shift=shift,output_pairs=False) ( [27 49 29] [ 0 27 49] (0, 1, 2), [ 0 0 27], (0, 1, 2) ) - sage: E.krylov_profile(M, shift=shift) + sage: E.krylov_basis(M,shift=shift) ( [27 49 29] [ 0 27 49] ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) ) - sage: shift = [3,0,2] sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) sage: rows @@ -19595,7 +19597,7 @@ cdef class Matrix(Matrix1): (7, 0, 2), (12, 2, 3), (10, 0, 3)] - sage: striped_krylov_matrix.with_permuted_rows(Permutation([x[0] for x in rows])) + sage: E.krylov_matrix(M,shift=shift) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19608,13 +19610,13 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_profile(M,shift=shift,output_pairs=False) + sage: E.krylov_basis(M,shift=shift,output_pairs=False) ( [50 58 0] [ 0 50 58] (0, 1, 2), [ 0 0 50], (0, 1, 2) ) - sage: E.krylov_profile(M,shift=shift) + sage: E.krylov_basis(M,shift=shift) ( [50 58 0] [ 0 50 58] @@ -19626,25 +19628,25 @@ cdef class Matrix(Matrix1): from sage.combinat.permutation import Permutation if not isinstance(M, Matrix): - raise TypeError("krylov_profile: M is not a matrix") + raise TypeError("krylov_basis: M is not a matrix") if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("krylov_profile: matrix M does not have correct dimensions.") + raise ValueError("krylov_basis: matrix M does not have correct dimensions.") if M.base_ring() != self.base_ring(): - raise ValueError("krylov_profile: matrix M does not have same base ring as E.") + raise ValueError("krylov_basis: matrix M does not have same base ring as E.") if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_profile: degree is not an integer.") + raise TypeError("krylov_basis: degree is not an integer.") if degree < 0: - raise ValueError("krylov_profile: degree bound cannot be negative.") + raise ValueError("krylov_basis: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_profile: shift is not a Z-vector of length {self.nrows()}.") + raise ValueError(f"krylov_basis: shift is not a Z-vector of length {self.nrows()}.") m = self.nrows() sigma = self.ncols() @@ -19739,7 +19741,54 @@ cdef class Matrix(Matrix1): return tuple(row_profile_K), R, col_profile - def linear_interpolation_basis(self, M, var_name, degree=None, shift=None): + def krylov_basis(self, M, shift=None, degree=None, output_pairs=True, algorithm='default'): + r""" + Compute the rank profile (row and column) of the block Krylov matrix + built from ``self`` and matrix ``M``. + + INPUT: + + - ``M`` -- square matrix used in the Krylov construction. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. + - ``shift`` -- list of ``self.nrows()`` integers (optional): priority + row shift. If ``None``, defaults to all zeroes. + + OUTPUT: + + A tuple (row_profile, pivot_matrix, column_profile): + - ``row_profile``: list of the first r row indices in the striped + Krylov matrix ``K`` corresponding to independent rows. If + ``output_pairs`` is True, then the output is the list of pairs + (c_i, d_i) where c_i is the corresponding row in the original matrix + and d_i the corresponding power of ``M``. + - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + - ``column_profile``: the first r independent column indices in + ``pivot_matrix`` where r is the rank of `K`. + """ + if algorithm == 'default': + if self.base_ring().order() == 2: + if self.nrows() <= 12: + algorithm = 'naive' + else: + algorithm = 'elimination' + elif self.base_ring().degree() == 1: + if self.nrows() <= 4: + algorithm = 'naive' + else: + algorithm = 'elimination' + else: + if self.nrows() <= 2: + algorithm = 'naive' + else: + algorithm = 'elimination' + if algorithm == 'naive': + return self._naive_krylov_basis(M, shift, degree, output_pairs) + elif algorithm == 'elimination': + return self._elimination_krylov_basis(M, shift, degree, output_pairs) + else: + raise ValueError("algorithm must be one of \"default\", \"naive\" or \"elimination\".") + + def krylov_kernel_basis(self, M, var_name='x', degree=None, shift=None, output_coefficients=False): r""" Return a shifted minimal linear interpolation basis for (``self``,``M``) in ``s``-Popov form with respect to a shift ``s``. @@ -19753,15 +19802,18 @@ cdef class Matrix(Matrix1): ``self.nrows()`` in K[x] that form a basis for solutions to the equation ``p_1 e_1 + ... + p_n e_n = 0``. - See https://arxiv.org/abs/1512.03503 + See [Kai1980] INPUT: - ``M`` -- square matrix for Krylov construction. - ``var_name`` -- variable name for the returned polynomial matrix - - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a suitable upper bound of ``self.ncols()`` is default. + - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a + suitable upper bound of ``self.ncols()`` is default. - ``shift`` -- list of self.nrows() integers (optional): controls - priority of rows. + priority of rows. If ``None``, defaults to all zeroes. + - ``output_coefficients`` -- If True, outputs the matrix as a sparse + block matrix of coefficients. OUTPUT: @@ -19782,8 +19834,8 @@ cdef class Matrix(Matrix1): [0 0 1] [0 0 0] sage: degree = E.ncols() - sage: striped_krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) - sage: striped_krylov_matrix + sage: krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) + sage: krylov_matrix [27 49 29] [50 58 0] [77 10 29] @@ -19796,48 +19848,63 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: basis = E.linear_interpolation_basis(M,'x') + sage: basis = E.krylov_kernel_basis(M,'x') sage: basis [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] + sage: basis_coeff = E.krylov_kernel_basis(M,'x',output_coefficients=True) + sage: basis_coeff + [82 76 0 40 0 0 1 0 0] + [13 57 0 3 1 0 0 0 0] + [96 96 1 0 0 0 0 0 0] sage: basis.is_popov() True sage: basis.degree() <= E.ncols() True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + sage: basis_coeff.augment(matrix.zero(R,E.nrows())) * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_profile(M)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shift = [0,3,6] - sage: basis = E.linear_interpolation_basis(M,'x',shift=shift) + sage: basis = E.krylov_kernel_basis(M,'x',shift=shift) sage: basis [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] + sage: basis_coeff = E.krylov_kernel_basis(M,'x',shift=shift,output_coefficients=True) + sage: basis_coeff + [ 0 0 0 0 0 0 0 0 0 1 0 0] + [70 1 0 72 0 0 60 0 0 0 0 0] + [69 0 1 72 0 0 60 0 0 0 0 0] sage: basis.is_popov(shifts=shift) True sage: basis.degree() <= E.ncols() True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_profile(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shift = [3,0,2] - sage: basis = E.linear_interpolation_basis(M,'x',shift=shift) + sage: basis = E.krylov_kernel_basis(M,'x',shift=shift) sage: basis [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] + sage: basis_coeff = E.krylov_kernel_basis(M,'x',shift=shift,output_coefficients=True) + sage: basis_coeff + [ 1 79 0 0 49 0 0 26 0 0 0 0] + [ 0 0 0 0 0 0 0 0 0 0 1 0] + [ 0 78 1 0 49 0 0 26 0 0 0 0] sage: basis.is_popov(shifts=shift) True sage: basis.degree() <= E.ncols() True - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(E.ncols()+1)]],subdivide=False) * striped_krylov_matrix == matrix.zero(R,E.nrows()) + sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_profile(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) True """ from sage.combinat.permutation import Permutation @@ -19846,11 +19913,11 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not isinstance(M, Matrix): - raise TypeError("linear_interpolation_basis: M is not a matrix") + raise TypeError("krylov_kernel_basis: M is not a matrix") if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("linear_interpolation_basis: matrix M does not have correct dimensions.") + raise ValueError("krylov_kernel_basis: matrix M does not have correct dimensions.") if M.base_ring() != self.base_ring(): - raise ValueError("linear_interpolation_basis: matrix M does not have same base ring as E.") + raise ValueError("krylov_kernel_basis: matrix M does not have same base ring as E.") if not isinstance(var_name, str): raise TypeError("var_name is not a string") @@ -19858,16 +19925,16 @@ cdef class Matrix(Matrix1): if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("linear_interpolation_basis: degree is not an integer.") + raise TypeError("krylov_kernel_basis: degree is not an integer.") if degree < 0: - raise ValueError("linear_interpolation_basis: degree bound cannot be negative.") + raise ValueError("krylov_kernel_basis: degree bound cannot be negative.") if shift is None or shift == 0: shift = (ZZ**self.nrows()).zero() if isinstance(shift, (list, tuple)): shift = (ZZ**self.nrows())(shift) if shift not in ZZ**self.nrows(): - raise ValueError(f"linear_interpolation_basis: shift is not a Z-vector of length {self.nrows()}.") + raise ValueError(f"krylov_kernel_basis: shift is not a Z-vector of length {self.nrows()}.") m = self.nrows() sigma = self.ncols() @@ -19875,14 +19942,14 @@ cdef class Matrix(Matrix1): poly_ring = PolynomialRing(self.base_ring(),var_name) # calculate krylov profile - row_profile, pivot, col_profile = self.krylov_profile(M, shift, degree) + row_profile, pivot, col_profile = self.krylov_basis(M, shift, degree) if len(row_profile) == 0: return matrix.identity(poly_ring,m) c, d = zip(*(row for row in row_profile)) - # degree_c = 0 or max d[k] such that c[k] = c + # degree_c = 0 or max d[k] + 1 such that c[k] = c inv_c = [None]*m degree_c = [0]*m for k in range(len(row_profile)): @@ -19909,57 +19976,66 @@ cdef class Matrix(Matrix1): D = matrix(D_rows) relation = D*C.inverse() - # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields - coeffs_map = [[{} for _ in range(m)] for _ in range(m)] - # add identity part of basis - for i in range(m): - coeffs_map[i][i][degree_c[i]] = self.base_ring().one() - # add relation part of basis - for col in range(relation.ncols()): - for row in range(m): - coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], self.base_ring().zero()) - relation[row,col] - # construct rows - basis_rows = [[None] * m for _ in range(m)] - if self.base_ring().degree() <= 1 or m*m*sigma < self.base_ring().order(): - # if base field is large, don't bother caching monomials - # or if polynomial construction is efficient (order = 1) - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = self.base_ring().zero() - else: - # construct a small polynomial (less costly), then shift - mindeg = min(coeffs_map[row][col].keys()) - maxdeg = max(coeffs_map[row][col].keys()) - entries = [self.base_ring().zero()] * (maxdeg-mindeg+1) - for deg, coeff in coeffs_map[row][col].items(): - entries[deg-mindeg] = coeff - basis_rows[row][col] = poly_ring(entries).shift(mindeg) - return matrix(poly_ring, basis_rows) + if output_coefficients: + coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c)+1)) + for i in range(m): + coefficients[i,i+degree_c[i]*m] = self.base_ring().one() + for col in range(relation.ncols()): + for row in range(m): + coefficients[row,c[col]+d[col]*m] -= relation[row,col] + return coefficients else: - # if base field is small, cache monomials to minimise number of constructions - monomial_cache = {} - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = poly_ring.zero() - elif len(coeffs_map[row][col]) == 1: - # monomial case - deg, coeff = coeffs_map[row][col].popitem() - if coeff not in monomial_cache: - monomial_cache[coeff] = poly_ring(coeff) - basis_rows[row][col] = monomial_cache[coeff].shift(deg) - else: - # general case - mindeg = min(coeffs_map[row][col].keys()) - poly = poly_ring.zero() - for deg, coeff in coeffs_map[row][col].items(): + # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields + coeffs_map = [[{} for _ in range(m)] for _ in range(m)] + # add identity part of basis + for i in range(m): + coeffs_map[i][i][degree_c[i]] = self.base_ring().one() + # add relation part of basis + for col in range(relation.ncols()): + for row in range(m): + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], self.base_ring().zero()) - relation[row,col] + # construct rows + basis_rows = [[None] * m for _ in range(m)] + if self.base_ring().degree() <= 1 or m*m*sigma < self.base_ring().order(): + # if base field is large, don't bother caching monomials + # or if polynomial construction is efficient (order = 1) + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = self.base_ring().zero() + else: + # construct a small polynomial (less costly), then shift + mindeg = min(coeffs_map[row][col].keys()) + maxdeg = max(coeffs_map[row][col].keys()) + entries = [self.base_ring().zero()] * (maxdeg-mindeg+1) + for deg, coeff in coeffs_map[row][col].items(): + entries[deg-mindeg] = coeff + basis_rows[row][col] = poly_ring(entries).shift(mindeg) + return matrix(poly_ring, basis_rows) + else: + # if base field is small, cache monomials to minimise number of constructions + monomial_cache = {} + for row in range(m): + for col in range(m): + if not coeffs_map[row][col]: + basis_rows[row][col] = poly_ring.zero() + elif len(coeffs_map[row][col]) == 1: + # monomial case + deg, coeff = coeffs_map[row][col].popitem() if coeff not in monomial_cache: monomial_cache[coeff] = poly_ring(coeff) - poly += monomial_cache[coeff].shift(deg-mindeg) - basis_rows[row][col] = poly.shift(mindeg) - # convert to matrix - return matrix(poly_ring,basis_rows) + basis_rows[row][col] = monomial_cache[coeff].shift(deg) + else: + # general case + mindeg = min(coeffs_map[row][col].keys()) + poly = poly_ring.zero() + for deg, coeff in coeffs_map[row][col].items(): + if coeff not in monomial_cache: + monomial_cache[coeff] = poly_ring(coeff) + poly += monomial_cache[coeff].shift(deg-mindeg) + basis_rows[row][col] = poly.shift(mindeg) + # convert to matrix + return matrix(poly_ring,basis_rows) # a limited number of access-only properties are provided for matrices @property From b9389ef5003a3023c9922159cdf0ce35d3f693e8 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 11 Aug 2025 20:04:32 +0200 Subject: [PATCH 27/57] changes from review, add support for multiple degrees (todo leftover) --- src/sage/matrix/matrix2.pyx | 445 +++++++++++++++++++----------------- 1 file changed, 229 insertions(+), 216 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 44965f81c4d..3efe21af167 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19111,11 +19111,19 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def krylov_matrix(self, M, shift=None, degree=None): + def krylov_matrix(self, M, shifts=None, degree=None): r""" - Return the block matrix built from the rows of ``self`` and the matrix ``M``, with rows ordered according to a priority defined by ``shift``. See [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912] and [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]." - - The following uses `E` to refer to ``self``, and `d` to refer to ``degree``. + Return the block matrix built from the rows of ``self`` and the matrix + ``M``, with rows ordered according to a priority defined by + ``shifts``. See + [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912] + and + [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]." + + The following example uses `E` to refer to ``self``, and `d` to refer + to ``degree`` (in the case where a single integer is provided). This is + the case where ``shifts`` is `[0,...,0]``; another shift will + correspond to some row permutation of this matrix. [ ] [ E ] @@ -19135,15 +19143,16 @@ cdef class Matrix(Matrix1): INPUT: - - ``M`` -- a square matrix of size equal to the number of columns of ``self``. + - ``M`` -- a square matrix of size equal to the number of columns of + ``self``. - ``degree`` -- integer, the maximum exponent for the Krylov matrix. - - ``shift`` -- list of ``self.nrows()`` integers (optional), row priority - shifts. If ``None``, defaults to all zeroes. + - ``shifts`` -- list of ``self.nrows()`` integers (optional), row + priority shifts. If ``None``, defaults to all zeroes. OUTPUT: - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by - shift. + ``shifts``. EXAMPLES:: @@ -19197,11 +19206,11 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - """ from sage.combinat.permutation import Permutation import math - from sage.matrix.constructor import matrix # for matrix.block + from sage.matrix.constructor import matrix + from sage.modules.free_module_element import vector if not isinstance(M, Matrix): raise TypeError("krylov_matrix: M is not a matrix") @@ -19211,53 +19220,60 @@ cdef class Matrix(Matrix1): raise ValueError("krylov_matrix: matrix M does not have same base ring as E.") if degree is None: - degree = self.ncols() - if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_matrix: degree is not an integer.") - if degree < 0: - raise ValueError("krylov_matrix: degree bound cannot be negative.") - - if shift is None or shift == 0: - shift = (ZZ**self.nrows()).zero() - if isinstance(shift, (list, tuple)): - shift = (ZZ**self.nrows())(shift) - if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_matrix: shift is not an integer vector of length {self.nrows()}.") + degree = vector(ZZ, [self.ncols()] * self.nrows()) + if isinstance(degree, (int, sage.rings.integer.Integer)): + degree = vector(ZZ, [degree] * self.nrows()) + if isinstance(degree, (list, tuple)): + degree = vector(ZZ, degree) + if degree not in ZZ**self.nrows(): + raise TypeError(f"krylov_matrix: degree must be an None, an integer, or an integer vector of length self.nrows().") + if self.nrows() > 0 and min(degree) < 0: + raise ValueError(f"krylov_matrix: degree must not contain a negative bound.") + + if shifts is None or shifts == 0: + shifts = (ZZ**self.nrows()).zero() + if isinstance(shifts, (list, tuple)): + shifts = (ZZ**self.nrows())(shifts) + if shifts not in ZZ**self.nrows(): + raise ValueError(f"krylov_matrix: shifts is not an integer vector of length {self.nrows()}.") m = self.nrows() # define priority and indexing functions # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shift[c] + d + priority = lambda c,d : shifts[c] + d index = lambda i : (i%m,i//m) # deduce permutation by sorting priorities # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_triplets = sorted([[priority(*index(i)),index(i),i] for i in range(m*(degree+1))]) + priority_triplets = [[priority(*index(i)),index(i)] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]] + priority_triplets = sorted([[t[0],t[1],i] for i,t in enumerate(priority_triplets)]) priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) # store 2 blocks for the Krylov matrix blocks = [self] + # keep track of indices + indices = [(i,0) for i in range(m)] + # for i from 0 to degree (inclusive), merge existing blocks, add E*M^i M_i = None - for i in range(math.ceil(math.log(degree + 1, 2))): + for i in range(math.ceil(math.log(max(degree, default=0) + 1, 2))): if M_i is None: M_i = M else: M_i = M_i * M_i - blocks = [matrix.block([[blocks[0]],[blocks[1]]],subdivide=False)] - blocks.append(blocks[0] * M_i) + blocks = [blocks[0].stack(blocks[1],subdivide=False)] + new_indices = [(row[0], row[1] + 2**i) for row in indices] + blocks.append((blocks[0].matrix_from_rows([i for i,x in enumerate(new_indices) if x[1] <= degree[x[0]]]) * M_i)) + indices.extend([x for x in new_indices if x[1] <= degree[x[0]]]) - if (degree + 1) & degree != 0: - blocks[1] = blocks[1].matrix_from_rows(range((degree + 1) * m - blocks[1].nrows())) - - # return block matrix permuted according to shift - krylov = matrix.block([[blocks[0]],[blocks[1]]],subdivide=False) + # return block matrix permuted according to shifts + krylov = blocks[0].stack(blocks[1],subdivide=False) if len(blocks) > 1 else blocks[0] krylov.permute_rows(priority_permutation) return krylov - def _naive_krylov_basis(self, M, shift=None, degree=None, output_pairs=True): + def _naive_krylov_basis(self, M, shifts, degree, output_row_indices): r""" Compute the rank profile (row and column) of the Krylov matrix built from ``self`` and matrix ``M``. @@ -19265,16 +19281,19 @@ cdef class Matrix(Matrix1): INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. - - ``shift`` -- list of ``self.nrows()`` integers (optional): priority row shift. If ``None``, defaults to all zeroes. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a + suitable upper bound of ``self.ncols()`` is default. + - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority + row shifts. If ``None``, defaults to all zeroes. OUTPUT: - - A tuple (row_profile, pivot_matrix, column_profile): - * row_profile: list of the first r row indices in the Krylov matrix ``K`` corresponding to independent rows - * pivot: the submatrix of ``K`` given by those rows - * column_profile: list of the first r independent column indices in ``pivot_matrix`` - where r is the rank of ``K``. + A tuple (row_profile, pivot_matrix, column_profile): + - pivot: the submatrix of ``K`` given by those rows + - row_profile: list of the first r row indices in the Krylov matrix + ``K`` corresponding to independent rows + - column_profile: list of the first r independent column indices in + ``pivot_matrix`` where r is the rank of ``K``. TESTS:: @@ -19304,17 +19323,17 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M, output_pairs=False) + sage: E.krylov_basis(M, output_row_indices=True) ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) + [27 49 29] + [50 58 0] + [ 0 27 49], (0, 1, 3), (0, 1, 2) ) sage: E.krylov_basis(M) ( - [27 49 29] - [50 58 0] - ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) + [27 49 29] + [50 58 0] + [ 0 27 49], ((0, 0), (1, 0), (0, 1)), (0, 1, 2) ) sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] @@ -19331,8 +19350,8 @@ cdef class Matrix(Matrix1): (10, 0, 3), (11, 1, 3), (12, 2, 3)] - sage: shift = [0,3,6] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: shifts = [0,3,6] + sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) sage: rows [(1, 0, 0), (4, 0, 1), @@ -19346,7 +19365,7 @@ cdef class Matrix(Matrix1): (6, 2, 1), (9, 2, 2), (12, 2, 3)] - sage: E.krylov_matrix(M,shift=shift) + sage: E.krylov_matrix(M,shifts=shifts) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19359,21 +19378,21 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_basis(M, shift=shift, output_pairs=False) + sage: E.krylov_basis(M, shifts=shifts, output_row_indices=True) ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) + [27 49 29] + [ 0 27 49] + [ 0 0 27], (0, 1, 2), (0, 1, 2) ) - sage: E.krylov_basis(M,shift=shift) + sage: E.krylov_basis(M,shifts=shifts) ( - [27 49 29] - [ 0 27 49] - ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) + [27 49 29] + [ 0 27 49] + [ 0 0 27], ((0, 0), (0, 1), (0, 2)), (0, 1, 2) ) - sage: shift = [3,0,2] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: shifts = [3,0,2] + sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) sage: rows [(2, 1, 0), (5, 1, 1), @@ -19387,7 +19406,7 @@ cdef class Matrix(Matrix1): (7, 0, 2), (12, 2, 3), (10, 0, 3)] - sage: E.krylov_matrix(M,shift=shift) + sage: E.krylov_matrix(M,shifts=shifts) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19400,45 +19419,24 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,shift=shift,output_pairs=False) + sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) + [50 58 0] + [ 0 50 58] + [ 0 0 50], (0, 1, 2), (0, 1, 2) ) - sage: E.krylov_basis(M,shift=shift) + sage: E.krylov_basis(M,shifts=shifts) ( - [50 58 0] - [ 0 50 58] - ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) + [50 58 0] + [ 0 50 58] + [ 0 0 50], ((1, 0), (1, 1), (1, 2)), (0, 1, 2) ) """ from sage.matrix.constructor import matrix - if not isinstance(M, Matrix): - raise TypeError("naive_krylov_basis: M is not a matrix") - if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("naive_krylov_basis: matrix M does not have correct dimensions.") - if M.base_ring() != self.base_ring(): - raise ValueError("naive_krylov_basis: matrix M does not have same base ring as E.") - - if degree is None: - degree = self.ncols() - if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("naive_krylov_basis: degree is not an integer.") - if degree < 0: - raise ValueError("naive_krylov_basis: degree bound cannot be negative.") - - if shift is None or shift == 0: - shift = (ZZ**self.nrows()).zero() - if isinstance(shift, (list, tuple)): - shift = (ZZ**self.nrows())(shift) - if shift not in ZZ**self.nrows(): - raise ValueError(f"naive_krylov_basis: shift is not an integer vector of length {self.nrows()}.") - m = self.nrows() - K = self.krylov_matrix(M,shift,degree) + K = self.krylov_matrix(M,shifts,degree) # calculate first independent rows of K row_profile = K.pivot_rows() @@ -19449,22 +19447,22 @@ cdef class Matrix(Matrix1): # calculate first independent columns of pivot col_profile = pivot.pivots() - if output_pairs: + if not output_row_indices: # define priority and indexing functions # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shift[c] + d + priority = lambda c,d : shifts[c] + d index = lambda i : (i%m,i//m) # deduce permutation by sorting priorities # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(degree+1))]) + priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]]) row_profile = tuple([priority_pairs[i][1] for i in row_profile]) - return row_profile,pivot,col_profile + return pivot, row_profile, col_profile - def _elimination_krylov_basis(self, M, shift=None, degree=None, output_pairs=True): + def _elimination_krylov_basis(self, M, shifts, degree, output_row_indices): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19472,19 +19470,21 @@ cdef class Matrix(Matrix1): INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. - - ``shift`` -- list of ``self.nrows()`` integers (optional): priority - row shift. If ``None``, defaults to all zeroes. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a + suitable upper bound of ``self.ncols()`` is default. + - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority + row shifts. If ``None``, defaults to all zeroes. OUTPUT: A tuple (row_profile, pivot_matrix, column_profile): + - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - ``row_profile``: list of the first r row indices in the striped Krylov matrix ``K`` corresponding to independent rows. If - ``output_pairs`` is True, then the output is the list of pairs + ``output_row_indices`` is False, then the output is the list of pairs (c_i, d_i) where c_i is the corresponding row in the original matrix - and d_i the corresponding power of ``M``. - - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + and d_i the corresponding power of ``M``. If True, the output is the + row indices in the Krylov matrix. - ``column_profile``: the first r independent column indices in ``pivot_matrix`` where r is the rank of `K`. @@ -19516,17 +19516,17 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,output_pairs=False) + sage: E.krylov_basis(M,output_row_indices=True) ( - [27 49 29] - [50 58 0] - (0, 1, 3), [ 0 27 49], (0, 1, 2) + [27 49 29] + [50 58 0] + [ 0 27 49], (0, 1, 3), (0, 1, 2) ) sage: E.krylov_basis(M) ( - [27 49 29] - [50 58 0] - ((0, 0), (1, 0), (0, 1)), [ 0 27 49], (0, 1, 2) + [27 49 29] + [50 58 0] + [ 0 27 49], ((0, 0), (1, 0), (0, 1)), (0, 1, 2) ) sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] sage: rows @@ -19542,8 +19542,8 @@ cdef class Matrix(Matrix1): (10, 0, 3), (11, 1, 3), (12, 2, 3)] - sage: shift = [0,3,6] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: shifts = [0,3,6] + sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) sage: rows [(1, 0, 0), (4, 0, 1), @@ -19557,7 +19557,7 @@ cdef class Matrix(Matrix1): (6, 2, 1), (9, 2, 2), (12, 2, 3)] - sage: E.krylov_matrix(M,shift=shift) + sage: E.krylov_matrix(M,shifts=shifts) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19570,20 +19570,20 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_basis(M,shift=shift,output_pairs=False) + sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) ( - [27 49 29] - [ 0 27 49] - (0, 1, 2), [ 0 0 27], (0, 1, 2) + [27 49 29] + [ 0 27 49] + [ 0 0 27], (0, 1, 2), (0, 1, 2) ) - sage: E.krylov_basis(M,shift=shift) + sage: E.krylov_basis(M,shifts=shifts) ( - [27 49 29] - [ 0 27 49] - ((0, 0), (0, 1), (0, 2)), [ 0 0 27], (0, 1, 2) + [27 49 29] + [ 0 27 49] + [ 0 0 27], ((0, 0), (0, 1), (0, 2)), (0, 1, 2) ) - sage: shift = [3,0,2] - sage: rows.sort(key=lambda x: (x[2] + shift[x[1]],x[1])) + sage: shifts = [3,0,2] + sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) sage: rows [(2, 1, 0), (5, 1, 1), @@ -19597,7 +19597,7 @@ cdef class Matrix(Matrix1): (7, 0, 2), (12, 2, 3), (10, 0, 3)] - sage: E.krylov_matrix(M,shift=shift) + sage: E.krylov_matrix(M,shifts=shifts) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19610,52 +19610,31 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,shift=shift,output_pairs=False) + sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) ( - [50 58 0] - [ 0 50 58] - (0, 1, 2), [ 0 0 50], (0, 1, 2) + [50 58 0] + [ 0 50 58] + [ 0 0 50], (0, 1, 2), (0, 1, 2) ) - sage: E.krylov_basis(M,shift=shift) + sage: E.krylov_basis(M,shifts=shifts) ( - [50 58 0] - [ 0 50 58] - ((1, 0), (1, 1), (1, 2)), [ 0 0 50], (0, 1, 2) + [50 58 0] + [ 0 50 58] + [ 0 0 50], ((1, 0), (1, 1), (1, 2)), (0, 1, 2) ) """ from sage.matrix.constructor import matrix import math from sage.combinat.permutation import Permutation - if not isinstance(M, Matrix): - raise TypeError("krylov_basis: M is not a matrix") - if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("krylov_basis: matrix M does not have correct dimensions.") - if M.base_ring() != self.base_ring(): - raise ValueError("krylov_basis: matrix M does not have same base ring as E.") - - if degree is None: - degree = self.ncols() - if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_basis: degree is not an integer.") - if degree < 0: - raise ValueError("krylov_basis: degree bound cannot be negative.") - - if shift is None or shift == 0: - shift = (ZZ**self.nrows()).zero() - if isinstance(shift, (list, tuple)): - shift = (ZZ**self.nrows())(shift) - if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_basis: shift is not a Z-vector of length {self.nrows()}.") - m = self.nrows() sigma = self.ncols() if m == 0: - return (),self,() + return self, (), () - # calculate row profile of self, with shift applied - self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shift[x-1],x-1))) + # calculate row profile of self, with shifts applied + self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shifts[x-1],x-1))) self_permuted = self.with_permuted_rows(self_permutation) row_profile_self_permuted = self_permuted.pivot_rows() @@ -19668,24 +19647,24 @@ cdef class Matrix(Matrix1): r = len(row_profile_self) if r == 0: - return (),self.matrix_from_rows([]),() + return self.matrix_from_rows([]),(),() R = self_permuted.matrix_from_rows(row_profile_self_permuted) M_L = None - for l in range(math.ceil(math.log(degree + 1, 2))): + for l in range(math.ceil(math.log(max(degree, default=0) + 1, 2))): L = pow(2,l) # adding 2^l to each degree - row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree] + row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree[x[0]]] if len(row_extension) == 0: break # concatenate two sequences (order preserved) k = row_profile_exhausted + row_profile_self + row_extension - # calculate sorting permutation, sort k by (shift[c]+d, c) - k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + # calculate sorting permutation, sort k by (shifts[c]+d, c) + k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shifts[k[x-1][0]] + k[x-1][1],k[x-1][0]))) # fast calculation of rows formed by indices in k if M_L is None: @@ -19693,7 +19672,7 @@ cdef class Matrix(Matrix1): else: M_L = M_L * M_L - R = matrix.block([[exhausted],[R],[R*M_L]],subdivide=False) + R = matrix.block([[exhausted],[R],[(R*M_L).matrix_from_rows([i for i in range(len(row_profile_self)) if row_profile_self[i][1] + L <= degree[row_profile_self[i][0]]])]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) R.permute_rows(k_perm) @@ -19701,7 +19680,7 @@ cdef class Matrix(Matrix1): row_profile_R = R.pivot_rows() r = len(row_profile_R) - if r == sigma and l < math.ceil(math.log(degree,2)): + if r == sigma and l < math.ceil(math.log(max(degree, default=0) + 1, 2)) - 1: tail = list(range(row_profile_R[-1]+1,R.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) @@ -19714,7 +19693,7 @@ cdef class Matrix(Matrix1): exhausted = R.matrix_from_rows(xmi) R = R.matrix_from_rows(imi) else: - if l == math.ceil(math.log(degree,2)): + if l == math.ceil(math.log(max(degree, default=0) + 1, 2)) - 1: row_profile_exhausted = [] exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_R] @@ -19726,22 +19705,22 @@ cdef class Matrix(Matrix1): elif exhausted.nrows() != 0: k = row_profile_exhausted + row_profile_self R = exhausted.stack(R) - k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shift[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shifts[k[x-1][0]] + k[x-1][1],k[x-1][0]))) R.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] col_profile = R.pivots() - if output_pairs: - return tuple(row_profile_self), R, col_profile + if not output_row_indices: + return R, tuple(row_profile_self), col_profile # convert c,d to actual position in striped Krylov matrix - phi = lambda c,d : sum(min(max(shift[c] - shift[i] + d + (i < c and shift[i] <= shift[c] + degree),0),degree+1) for i in range(m)) + phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + max(degree)),0),max(degree)+1) for i in range(m)) # TODO: fix row_profile_K = [phi(*row) for row in row_profile_self] - return tuple(row_profile_K), R, col_profile + return R, tuple(row_profile_K), col_profile - def krylov_basis(self, M, shift=None, degree=None, output_pairs=True, algorithm='default'): + def krylov_basis(self, M, shifts=None, degree=None, output_row_indices=False, algorithm=None): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19749,23 +19728,57 @@ cdef class Matrix(Matrix1): INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default. - - ``shift`` -- list of ``self.nrows()`` integers (optional): priority - row shift. If ``None``, defaults to all zeroes. + - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a + suitable upper bound of ``self.ncols()`` is default. + - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority + row shifts. If ``None``, defaults to all zeroes. + - ``output_row_indices`` -- determines whether ``row_profile`` is + returned as pairs of row indices in ``self`` and degrees of ``M`` + (``False``), or row indices in the Krylov matrix (``True``). + - ``algorithm`` -- either 'naive', 'elimination', or None (let + Sage choose). OUTPUT: A tuple (row_profile, pivot_matrix, column_profile): + - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - ``row_profile``: list of the first r row indices in the striped Krylov matrix ``K`` corresponding to independent rows. If - ``output_pairs`` is True, then the output is the list of pairs + ``output_row_indices`` is False, then the output is the list of pairs (c_i, d_i) where c_i is the corresponding row in the original matrix - and d_i the corresponding power of ``M``. - - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` + and d_i the corresponding power of ``M``. If True, the output is the + row indices in the Krylov matrix. - ``column_profile``: the first r independent column indices in ``pivot_matrix`` where r is the rank of `K`. """ - if algorithm == 'default': + from sage.modules.free_module_element import vector + + if not isinstance(M, Matrix): + raise TypeError("_naive_krylov_basis: M is not a matrix") + if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + raise ValueError("_naive_krylov_basis: matrix M does not have correct dimensions.") + if M.base_ring() != self.base_ring(): + raise ValueError("naive_krylov_basis: matrix M does not have same base ring as E.") + + if degree is None: + degree = vector(ZZ, [self.ncols()] * self.nrows()) + if isinstance(degree, (int, sage.rings.integer.Integer)): + degree = vector(ZZ, [degree] * self.nrows()) + if isinstance(degree, (list, tuple)): + degree = vector(ZZ, degree) + if degree not in ZZ**self.nrows(): + raise TypeError(f"_naive_krylov_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") + if self.nrows() > 0 and min(degree) < 0: + raise ValueError(f"_naive_krylov_basis: degree must not contain a negative bound.") + + if shifts is None or shifts == 0: + shifts = (ZZ**self.nrows()).zero() + if isinstance(shifts, (list, tuple)): + shifts = (ZZ**self.nrows())(shifts) + if shifts not in ZZ**self.nrows(): + raise ValueError(f"_naive_krylov_basis: shifts is not an integer vector of length {self.nrows()}.") + + if algorithm is None: if self.base_ring().order() == 2: if self.nrows() <= 12: algorithm = 'naive' @@ -19782,38 +19795,38 @@ cdef class Matrix(Matrix1): else: algorithm = 'elimination' if algorithm == 'naive': - return self._naive_krylov_basis(M, shift, degree, output_pairs) + return self._naive_krylov_basis(M, shifts, degree, output_row_indices) elif algorithm == 'elimination': - return self._elimination_krylov_basis(M, shift, degree, output_pairs) + return self._elimination_krylov_basis(M, shifts, degree, output_row_indices) else: - raise ValueError("algorithm must be one of \"default\", \"naive\" or \"elimination\".") + raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, var_name='x', degree=None, shift=None, output_coefficients=False): + def krylov_kernel_basis(self, M, degree=None, shifts=None, output_coefficients=False, var='x'): r""" - Return a shifted minimal linear interpolation basis for - (``self``,``M``) in ``s``-Popov form with respect to a shift ``s``. + Return a shifted minimal krylov kernel basis for (``self``,``M``) in + ``s``-Popov form with respect to a set of shifts ``s``. - Given a K[x]-module V defined by multiplication matrix ``M``, i.e. elements - are vectors of length n in K, scalars are polynomials in K[x], and - scalar multiplication is defined by p v := v p(``M``), ``self`` represents - ``self.nrows()`` elements e_1, ..., e_m in V: + Given a K[x]-module V defined by multiplication matrix ``M``, i.e. + elements are vectors of length n in K, scalars are polynomials in K[x], + and scalar multiplication is defined by p v := v p(``M``), ``self`` + represents ``self.nrows()`` elements e_1, ..., e_m in V: A linear interpolation basis is a set of tuples of length ``self.nrows()`` in K[x] that form a basis for solutions to the equation ``p_1 e_1 + ... + p_n e_n = 0``. - See [Kai1980] + See [Kai1980]_ INPUT: - ``M`` -- square matrix for Krylov construction. - - ``var_name`` -- variable name for the returned polynomial matrix + - ``shifts`` -- list of self.nrows() integers (optional): controls + priority of rows. If ``None``, defaults to all zeroes. - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a suitable upper bound of ``self.ncols()`` is default. - - ``shift`` -- list of self.nrows() integers (optional): controls - priority of rows. If ``None``, defaults to all zeroes. - ``output_coefficients`` -- If True, outputs the matrix as a sparse block matrix of coefficients. + - ``var`` -- variable name for the returned polynomial matrix OUTPUT: @@ -19834,7 +19847,7 @@ cdef class Matrix(Matrix1): [0 0 1] [0 0 0] sage: degree = E.ncols() - sage: krylov_matrix = matrix.block([[E*M^i] for i in range(degree+1)],subdivide=False) + sage: krylov_matrix = E.krylov_matrix(M) sage: krylov_matrix [27 49 29] [50 58 0] @@ -19848,12 +19861,12 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: basis = E.krylov_kernel_basis(M,'x') + sage: basis = E.krylov_kernel_basis(M) sage: basis [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] - sage: basis_coeff = E.krylov_kernel_basis(M,'x',output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M,output_coefficients=True) sage: basis_coeff [82 76 0 40 0 0 1 0 0] [13 57 0 3 1 0 0 0 0] @@ -19864,47 +19877,47 @@ cdef class Matrix(Matrix1): True sage: basis_coeff.augment(matrix.zero(R,E.nrows())) * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_basis(M)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True - sage: shift = [0,3,6] - sage: basis = E.krylov_kernel_basis(M,'x',shift=shift) + sage: shifts = [0,3,6] + sage: basis = E.krylov_kernel_basis(M,shifts=shifts) sage: basis [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] - sage: basis_coeff = E.krylov_kernel_basis(M,'x',shift=shift,output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts,output_coefficients=True) sage: basis_coeff [ 0 0 0 0 0 0 0 0 0 1 0 0] [70 1 0 72 0 0 60 0 0 0 0 0] [69 0 1 72 0 0 60 0 0 0 0 0] - sage: basis.is_popov(shifts=shift) + sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_basis(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M,shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True - sage: shift = [3,0,2] - sage: basis = E.krylov_kernel_basis(M,'x',shift=shift) + sage: shifts = [3,0,2] + sage: basis = E.krylov_kernel_basis(M,shifts=shifts) sage: basis [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] - sage: basis_coeff = E.krylov_kernel_basis(M,'x',shift=shift,output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts,output_coefficients=True) sage: basis_coeff [ 1 79 0 0 49 0 0 26 0 0 0 0] [ 0 0 0 0 0 0 0 0 0 0 1 0] [ 0 78 1 0 49 0 0 26 0 0 0 0] - sage: basis.is_popov(shifts=shift) + sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_basis(M,shift=shift)[0]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M,shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True """ from sage.combinat.permutation import Permutation @@ -19919,30 +19932,30 @@ cdef class Matrix(Matrix1): if M.base_ring() != self.base_ring(): raise ValueError("krylov_kernel_basis: matrix M does not have same base ring as E.") - if not isinstance(var_name, str): - raise TypeError("var_name is not a string") + if not isinstance(var, str): + raise TypeError("var is not a string") if degree is None: degree = self.ncols() if not isinstance(degree, (int, sage.rings.integer.Integer)): raise TypeError("krylov_kernel_basis: degree is not an integer.") - if degree < 0: + if self.nrows() > 0 and degree < 0: raise ValueError("krylov_kernel_basis: degree bound cannot be negative.") - if shift is None or shift == 0: - shift = (ZZ**self.nrows()).zero() - if isinstance(shift, (list, tuple)): - shift = (ZZ**self.nrows())(shift) - if shift not in ZZ**self.nrows(): - raise ValueError(f"krylov_kernel_basis: shift is not a Z-vector of length {self.nrows()}.") + if shifts is None or shifts == 0: + shifts = (ZZ**self.nrows()).zero() + if isinstance(shifts, (list, tuple)): + shifts = (ZZ**self.nrows())(shifts) + if shifts not in ZZ**self.nrows(): + raise ValueError(f"krylov_kernel_basis: shifts is not a Z-vector of length {self.nrows()}.") m = self.nrows() sigma = self.ncols() - poly_ring = PolynomialRing(self.base_ring(),var_name) + poly_ring = PolynomialRing(self.base_ring(),var) # calculate krylov profile - row_profile, pivot, col_profile = self.krylov_basis(M, shift, degree) + pivot, row_profile, col_profile = self.krylov_basis(M, shifts, degree) if len(row_profile) == 0: return matrix.identity(poly_ring,m) @@ -19977,7 +19990,7 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() if output_coefficients: - coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c)+1)) + coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c, default=0)+1), sparse=True) for i in range(m): coefficients[i,i+degree_c[i]*m] = self.base_ring().one() for col in range(relation.ncols()): From 4a1084ec69b492453d59551f564deb583e988119 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 13 Aug 2025 17:33:20 +0200 Subject: [PATCH 28/57] finish multiple degree bound modifications --- src/sage/matrix/matrix2.pyx | 86 ++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 3efe21af167..7ce08a27b30 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19145,9 +19145,11 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``degree`` -- integer, the maximum exponent for the Krylov matrix. - ``shifts`` -- list of ``self.nrows()`` integers (optional), row priority shifts. If ``None``, defaults to all zeroes. + - ``degree`` -- the maximum power or list of ``self.nrows()`` maximum + powers of ``M`` for each row in ``self`` in the output. If None, + ``self.ncols()`` is chosen by default for all rows. OUTPUT: @@ -19270,21 +19272,30 @@ cdef class Matrix(Matrix1): # return block matrix permuted according to shifts krylov = blocks[0].stack(blocks[1],subdivide=False) if len(blocks) > 1 else blocks[0] - krylov.permute_rows(priority_permutation) - return krylov + return krylov.with_permuted_rows(priority_permutation) def _naive_krylov_basis(self, M, shifts, degree, output_row_indices): r""" Compute the rank profile (row and column) of the Krylov matrix built from ``self`` and matrix ``M``. + .. WARNING:: + + If the degree bounds are not true upper bounds, no guarantees are + made on the value of the output. + INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a - suitable upper bound of ``self.ncols()`` is default. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. + - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + on the power of ``M`` for each row in ``self`` in the + ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper + bound of ``self.ncols()`` is default for all rows. + - ``output_row_indices`` -- determines whether the output row indices + are pairs of row indices in ``self`` and degrees of ``M`` (False) or + row indices in the Krylov matrix (True). OUTPUT: @@ -19467,13 +19478,23 @@ cdef class Matrix(Matrix1): Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. + .. WARNING:: + + If the degree bounds are not true upper bounds, no guarantees are + made on the value of the output. + INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a - suitable upper bound of ``self.ncols()`` is default. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. + - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + on the power of ``M`` for each row in ``self`` in the + ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper + bound of ``self.ncols()`` is default for all rows. + - ``output_row_indices`` -- determines whether the output row indices + are pairs of row indices in ``self`` and degrees of ``M`` (False) or + row indices in the Krylov matrix (True). OUTPUT: @@ -19654,9 +19675,9 @@ cdef class Matrix(Matrix1): M_L = None for l in range(math.ceil(math.log(max(degree, default=0) + 1, 2))): - L = pow(2,l) + L = pow(2, l) # adding 2^l to each degree - row_extension = [(x[0],x[1] + L) for x in row_profile_self if x[1] + L <= degree[x[0]]] + row_extension = [(x[0], x[1] + L) for x in row_profile_self if x[1] + L <= degree[x[0]]] if len(row_extension) == 0: break @@ -19715,7 +19736,7 @@ cdef class Matrix(Matrix1): return R, tuple(row_profile_self), col_profile # convert c,d to actual position in striped Krylov matrix - phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + max(degree)),0),max(degree)+1) for i in range(m)) # TODO: fix + phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degree[i] + 1) for i in range(m)) row_profile_K = [phi(*row) for row in row_profile_self] return R, tuple(row_profile_K), col_profile @@ -19725,13 +19746,21 @@ cdef class Matrix(Matrix1): Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. + .. WARNING:: + + If the degree bounds are not true upper bounds, no guarantees are + made on the value of the output, including whether it is consistent + between different algorithms. + INPUT: - ``M`` -- square matrix used in the Krylov construction. - - ``degree`` -- maximum power of ``M`` in Krylov matrix. If None, a - suitable upper bound of ``self.ncols()`` is default. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. + - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + on the power of ``M`` for each row in ``self`` in the + ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper + bound of ``self.ncols()`` is default for all rows. - ``output_row_indices`` -- determines whether ``row_profile`` is returned as pairs of row indices in ``self`` and degrees of ``M`` (``False``), or row indices in the Krylov matrix (``True``). @@ -19754,11 +19783,11 @@ cdef class Matrix(Matrix1): from sage.modules.free_module_element import vector if not isinstance(M, Matrix): - raise TypeError("_naive_krylov_basis: M is not a matrix") + raise TypeError("krylov_basis: M is not a matrix") if M.nrows() != self.ncols() or M.ncols() != self.ncols(): - raise ValueError("_naive_krylov_basis: matrix M does not have correct dimensions.") + raise ValueError("krylov_basis: matrix M does not have correct dimensions.") if M.base_ring() != self.base_ring(): - raise ValueError("naive_krylov_basis: matrix M does not have same base ring as E.") + raise ValueError("krylov_basis: matrix M does not have same base ring as E.") if degree is None: degree = vector(ZZ, [self.ncols()] * self.nrows()) @@ -19767,16 +19796,16 @@ cdef class Matrix(Matrix1): if isinstance(degree, (list, tuple)): degree = vector(ZZ, degree) if degree not in ZZ**self.nrows(): - raise TypeError(f"_naive_krylov_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") + raise TypeError(f"krylov_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") if self.nrows() > 0 and min(degree) < 0: - raise ValueError(f"_naive_krylov_basis: degree must not contain a negative bound.") + raise ValueError(f"krylov_basis: degree must not contain a negative bound.") if shifts is None or shifts == 0: shifts = (ZZ**self.nrows()).zero() if isinstance(shifts, (list, tuple)): shifts = (ZZ**self.nrows())(shifts) if shifts not in ZZ**self.nrows(): - raise ValueError(f"_naive_krylov_basis: shifts is not an integer vector of length {self.nrows()}.") + raise ValueError(f"krylov_basis: shifts is not an integer vector of length {self.nrows()}.") if algorithm is None: if self.base_ring().order() == 2: @@ -19801,7 +19830,7 @@ cdef class Matrix(Matrix1): else: raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, degree=None, shifts=None, output_coefficients=False, var='x'): + def krylov_kernel_basis(self, M, shifts=None, degree=None, output_coefficients=False, var='x'): r""" Return a shifted minimal krylov kernel basis for (``self``,``M``) in ``s``-Popov form with respect to a set of shifts ``s``. @@ -19922,6 +19951,7 @@ cdef class Matrix(Matrix1): """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix + from sage.modules.free_module_element import vector from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing # INPUT VALIDATION @@ -19936,11 +19966,15 @@ cdef class Matrix(Matrix1): raise TypeError("var is not a string") if degree is None: - degree = self.ncols() - if not isinstance(degree, (int, sage.rings.integer.Integer)): - raise TypeError("krylov_kernel_basis: degree is not an integer.") - if self.nrows() > 0 and degree < 0: - raise ValueError("krylov_kernel_basis: degree bound cannot be negative.") + degree = vector(ZZ, [self.ncols()] * self.nrows()) + if isinstance(degree, (int, sage.rings.integer.Integer)): + degree = vector(ZZ, [degree] * self.nrows()) + if isinstance(degree, (list, tuple)): + degree = vector(ZZ, degree) + if degree not in ZZ**self.nrows(): + raise TypeError(f"krylov_kernel_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") + if self.nrows() > 0 and min(degree) < 0: + raise ValueError(f"krylov_kernel_basis: degree must not contain a negative bound.") if shifts is None or shifts == 0: shifts = (ZZ**self.nrows()).zero() @@ -19990,7 +20024,7 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() if output_coefficients: - coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c, default=0)+1), sparse=True) + coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c, default=0)+1)) for i in range(m): coefficients[i,i+degree_c[i]*m] = self.base_ring().one() for col in range(relation.ncols()): @@ -20037,7 +20071,7 @@ cdef class Matrix(Matrix1): deg, coeff = coeffs_map[row][col].popitem() if coeff not in monomial_cache: monomial_cache[coeff] = poly_ring(coeff) - basis_rows[row][col] = monomial_cache[coeff].shift(deg) + basis_rows[row][col] = monomial_cache[coeff].shift (deg) else: # general case mindeg = min(coeffs_map[row][col].keys()) From 6d2afd49d8fb8ac4929a674d4d64bbbb12f4127e Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Wed, 20 Aug 2025 17:06:16 +0200 Subject: [PATCH 29/57] review changes --- src/sage/matrix/matrix2.pyx | 457 +++++++++++++++--------------------- 1 file changed, 190 insertions(+), 267 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 7ce08a27b30..3c2e8f692ac 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19116,9 +19116,9 @@ cdef class Matrix(Matrix1): Return the block matrix built from the rows of ``self`` and the matrix ``M``, with rows ordered according to a priority defined by ``shifts``. See - [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912] + [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912]_ and - [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]." + [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]_." The following example uses `E` to refer to ``self``, and `d` to refer to ``degree`` (in the case where a single integer is provided). This is @@ -19212,34 +19212,38 @@ cdef class Matrix(Matrix1): from sage.combinat.permutation import Permutation import math from sage.matrix.constructor import matrix - from sage.modules.free_module_element import vector + from sage.modules.free_module_element import vector, FreeModuleElement + + E = self + + # INPUT VALIDATION + if not (E.base_ring() in _Fields and E.base_ring().is_exact()): + raise TypeError("krylov_matrix: matrix entries must come from an exact field") if not isinstance(M, Matrix): raise TypeError("krylov_matrix: M is not a matrix") - if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + if M.nrows() != E.ncols() or M.ncols() != E.ncols(): raise ValueError("krylov_matrix: matrix M does not have correct dimensions.") - if M.base_ring() != self.base_ring(): - raise ValueError("krylov_matrix: matrix M does not have same base ring as E.") + if M.base_ring() != E.base_ring(): + E, M = coercion_model.canonical_coercion(E, M) if degree is None: - degree = vector(ZZ, [self.ncols()] * self.nrows()) - if isinstance(degree, (int, sage.rings.integer.Integer)): - degree = vector(ZZ, [degree] * self.nrows()) - if isinstance(degree, (list, tuple)): - degree = vector(ZZ, degree) - if degree not in ZZ**self.nrows(): - raise TypeError(f"krylov_matrix: degree must be an None, an integer, or an integer vector of length self.nrows().") - if self.nrows() > 0 and min(degree) < 0: + degree = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degree, (FreeModuleElement, list, tuple)): + degree = (ZZ**E.nrows())(degree) + else: + degree = (ZZ**E.nrows())([degree] * E.nrows()) + if E.nrows() > 0 and min(degree) < 0: raise ValueError(f"krylov_matrix: degree must not contain a negative bound.") - if shifts is None or shifts == 0: - shifts = (ZZ**self.nrows()).zero() - if isinstance(shifts, (list, tuple)): - shifts = (ZZ**self.nrows())(shifts) - if shifts not in ZZ**self.nrows(): - raise ValueError(f"krylov_matrix: shifts is not an integer vector of length {self.nrows()}.") + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"krylov_matrix: shifts is not an integer vector of length {E.nrows()}.") - m = self.nrows() + m = E.nrows() # define priority and indexing functions # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] @@ -19253,7 +19257,7 @@ cdef class Matrix(Matrix1): priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) # store 2 blocks for the Krylov matrix - blocks = [self] + blocks = [E] # keep track of indices indices = [(i,0) for i in range(m)] @@ -19274,7 +19278,7 @@ cdef class Matrix(Matrix1): krylov = blocks[0].stack(blocks[1],subdivide=False) if len(blocks) > 1 else blocks[0] return krylov.with_permuted_rows(priority_permutation) - def _naive_krylov_basis(self, M, shifts, degree, output_row_indices): + def _krylov_basis_naive(self, M, shifts, degree, output_rows): r""" Compute the rank profile (row and column) of the Krylov matrix built from ``self`` and matrix ``M``. @@ -19293,18 +19297,19 @@ cdef class Matrix(Matrix1): on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_row_indices`` -- determines whether the output row indices + - ``output_rows`` -- determines whether the output row indices are pairs of row indices in ``self`` and degrees of ``M`` (False) or row indices in the Krylov matrix (True). OUTPUT: - A tuple (row_profile, pivot_matrix, column_profile): - - pivot: the submatrix of ``K`` given by those rows - - row_profile: list of the first r row indices in the Krylov matrix - ``K`` corresponding to independent rows - - column_profile: list of the first r independent column indices in - ``pivot_matrix`` where r is the rank of ``K``. + - krylov_basis: the submatrix of the krylov matrix of ``self`` and + ``M`` formed by its lexicographically-first independent rows. + - row_profile (returned if ``output_rows`` is ``True``): list of the + ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov + basis where ``i`` is the row index of the row in the krylov matrix, + ``c`` is the corresponding row in ``self`` and ``d`` is the + corresponding power of ``M``. TESTS:: @@ -19334,19 +19339,12 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M, output_row_indices=True) - ( - [27 49 29] - [50 58 0] - [ 0 27 49], (0, 1, 3), (0, 1, 2) - ) - sage: E.krylov_basis(M) + sage: E.krylov_basis(M,algorithm='naive') ( [27 49 29] [50 58 0] - [ 0 27 49], ((0, 0), (1, 0), (0, 1)), (0, 1, 2) + [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] sage: rows [(1, 0, 0), @@ -19389,19 +19387,12 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_basis(M, shifts=shifts, output_row_indices=True) - ( - [27 49 29] - [ 0 27 49] - [ 0 0 27], (0, 1, 2), (0, 1, 2) - ) - sage: E.krylov_basis(M,shifts=shifts) + sage: E.krylov_basis(M,shifts=shifts,algorithm='naive') ( [27 49 29] [ 0 27 49] - [ 0 0 27], ((0, 0), (0, 1), (0, 2)), (0, 1, 2) + [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) - sage: shifts = [3,0,2] sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) sage: rows @@ -19430,17 +19421,11 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) + sage: E.krylov_basis(M,shifts=shifts,algorithm='naive') ( [50 58 0] [ 0 50 58] - [ 0 0 50], (0, 1, 2), (0, 1, 2) - ) - sage: E.krylov_basis(M,shifts=shifts) - ( - [50 58 0] - [ 0 50 58] - [ 0 0 50], ((1, 0), (1, 1), (1, 2)), (0, 1, 2) + [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) ) """ from sage.matrix.constructor import matrix @@ -19455,25 +19440,23 @@ cdef class Matrix(Matrix1): # construct submatrix pivot = K.matrix_from_rows(row_profile) - # calculate first independent columns of pivot - col_profile = pivot.pivots() - - if not output_row_indices: + if not output_rows: + return pivot - # define priority and indexing functions - # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shifts[c] + d - index = lambda i : (i%m,i//m) + # define priority and indexing functions + # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] + priority = lambda c,d : shifts[c] + d + index = lambda i : (i%m,i//m) - # deduce permutation by sorting priorities - # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_pairs = sorted([[priority(*index(i)),index(i)] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]]) + # deduce permutation by sorting priorities + # rows should be ordered by ascending priority, then by associated row number in E (i%m) + priority_pairs = sorted([[priority(*index(i)),index(i),i] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]]) - row_profile = tuple([priority_pairs[i][1] for i in row_profile]) + row_profile = tuple([(*priority_pairs[i][1], i) for i in row_profile]) - return pivot, row_profile, col_profile + return pivot, row_profile - def _elimination_krylov_basis(self, M, shifts, degree, output_row_indices): + def _krylov_basis_elimination(self, M, shifts, degree, output_rows): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19492,22 +19475,19 @@ cdef class Matrix(Matrix1): on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_row_indices`` -- determines whether the output row indices + - ``output_rows`` -- determines whether the output row indices are pairs of row indices in ``self`` and degrees of ``M`` (False) or row indices in the Krylov matrix (True). OUTPUT: - A tuple (row_profile, pivot_matrix, column_profile): - - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - - ``row_profile``: list of the first r row indices in the striped - Krylov matrix ``K`` corresponding to independent rows. If - ``output_row_indices`` is False, then the output is the list of pairs - (c_i, d_i) where c_i is the corresponding row in the original matrix - and d_i the corresponding power of ``M``. If True, the output is the - row indices in the Krylov matrix. - - ``column_profile``: the first r independent column indices in - ``pivot_matrix`` where r is the rank of `K`. + - krylov_basis: the submatrix of the krylov matrix of ``self`` and + ``M`` formed by its lexicographically-first independent rows. + - row_profile (returned if ``output_rows`` is ``True``): list of the + ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov + basis where ``i`` is the row index of the row in the krylov matrix, + ``c`` is the corresponding row in ``self`` and ``d`` is the + corresponding power of ``M``. TESTS:: @@ -19537,17 +19517,11 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,output_row_indices=True) + sage: E.krylov_basis(M,output_rows=True,algorithm='elimination') ( [27 49 29] [50 58 0] - [ 0 27 49], (0, 1, 3), (0, 1, 2) - ) - sage: E.krylov_basis(M) - ( - [27 49 29] - [50 58 0] - [ 0 27 49], ((0, 0), (1, 0), (0, 1)), (0, 1, 2) + [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] sage: rows @@ -19591,17 +19565,11 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) - ( - [27 49 29] - [ 0 27 49] - [ 0 0 27], (0, 1, 2), (0, 1, 2) - ) - sage: E.krylov_basis(M,shifts=shifts) + sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') ( [27 49 29] [ 0 27 49] - [ 0 0 27], ((0, 0), (0, 1), (0, 2)), (0, 1, 2) + [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) sage: shifts = [3,0,2] sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) @@ -19631,17 +19599,11 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,output_row_indices=True) + sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') ( [50 58 0] [ 0 50 58] - [ 0 0 50], (0, 1, 2), (0, 1, 2) - ) - sage: E.krylov_basis(M,shifts=shifts) - ( - [50 58 0] - [ 0 50 58] - [ 0 0 50], ((1, 0), (1, 1), (1, 2)), (0, 1, 2) + [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) ) """ from sage.matrix.constructor import matrix @@ -19652,7 +19614,7 @@ cdef class Matrix(Matrix1): sigma = self.ncols() if m == 0: - return self, (), () + return (self, ()) if output_rows else self # calculate row profile of self, with shifts applied self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shifts[x-1],x-1))) @@ -19668,7 +19630,7 @@ cdef class Matrix(Matrix1): r = len(row_profile_self) if r == 0: - return self.matrix_from_rows([]),(),() + return (self.matrix_from_rows([]),()) if output_rows else self.matrix_from_rows([]) R = self_permuted.matrix_from_rows(row_profile_self_permuted) @@ -19730,18 +19692,17 @@ cdef class Matrix(Matrix1): R.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] - col_profile = R.pivots() - if not output_row_indices: - return R, tuple(row_profile_self), col_profile + if not output_rows: + return R # convert c,d to actual position in striped Krylov matrix phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degree[i] + 1) for i in range(m)) - row_profile_K = [phi(*row) for row in row_profile_self] + row_profile = tuple([(*row,phi(*row)) for row in row_profile_self]) - return R, tuple(row_profile_K), col_profile + return R, row_profile - def krylov_basis(self, M, shifts=None, degree=None, output_row_indices=False, algorithm=None): + def krylov_basis(self, M, shifts=None, degree=None, output_rows=True, algorithm=None): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19761,7 +19722,7 @@ cdef class Matrix(Matrix1): on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_row_indices`` -- determines whether ``row_profile`` is + - ``output_rows`` -- determines whether ``row_profile`` is returned as pairs of row indices in ``self`` and degrees of ``M`` (``False``), or row indices in the Krylov matrix (``True``). - ``algorithm`` -- either 'naive', 'elimination', or None (let @@ -19769,68 +19730,69 @@ cdef class Matrix(Matrix1): OUTPUT: - A tuple (row_profile, pivot_matrix, column_profile): - - ``pivot_matrix``: the submatrix of ``K`` given by ``row_profile`` - - ``row_profile``: list of the first r row indices in the striped - Krylov matrix ``K`` corresponding to independent rows. If - ``output_row_indices`` is False, then the output is the list of pairs - (c_i, d_i) where c_i is the corresponding row in the original matrix - and d_i the corresponding power of ``M``. If True, the output is the - row indices in the Krylov matrix. - - ``column_profile``: the first r independent column indices in - ``pivot_matrix`` where r is the rank of `K`. + - krylov_basis: the submatrix of the krylov matrix of ``self`` and + ``M`` formed by its lexicographically-first independent rows. + - row_profile (returned if ``output_rows`` is ``True``): list of the + ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov + basis where ``i`` is the row index of the row in the krylov matrix, + ``c`` is the corresponding row in ``self`` and ``d`` is the + corresponding power of ``M``. """ from sage.modules.free_module_element import vector + E = self + + # INPUT VALIDATION + if not (E.base_ring() in _Fields and E.base_ring().is_exact()): + raise TypeError("krylov_basis: matrix entries must come from an exact field") + if not isinstance(M, Matrix): raise TypeError("krylov_basis: M is not a matrix") - if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + if M.nrows() != E.ncols() or M.ncols() != E.ncols(): raise ValueError("krylov_basis: matrix M does not have correct dimensions.") - if M.base_ring() != self.base_ring(): - raise ValueError("krylov_basis: matrix M does not have same base ring as E.") + if M.base_ring() != E.base_ring(): + E, M = coercion_model.canonical_coercion(E, M) if degree is None: - degree = vector(ZZ, [self.ncols()] * self.nrows()) - if isinstance(degree, (int, sage.rings.integer.Integer)): - degree = vector(ZZ, [degree] * self.nrows()) - if isinstance(degree, (list, tuple)): - degree = vector(ZZ, degree) - if degree not in ZZ**self.nrows(): - raise TypeError(f"krylov_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") - if self.nrows() > 0 and min(degree) < 0: + degree = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degree, (FreeModuleElement, list, tuple)): + degree = (ZZ**E.nrows())(degree) + else: + degree = (ZZ**E.nrows())([degree] * E.nrows()) + if E.nrows() > 0 and min(degree) < 0: raise ValueError(f"krylov_basis: degree must not contain a negative bound.") - if shifts is None or shifts == 0: - shifts = (ZZ**self.nrows()).zero() - if isinstance(shifts, (list, tuple)): - shifts = (ZZ**self.nrows())(shifts) - if shifts not in ZZ**self.nrows(): - raise ValueError(f"krylov_basis: shifts is not an integer vector of length {self.nrows()}.") + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"krylov_basis: shifts is not an integer vector of length {E.nrows()}.") if algorithm is None: - if self.base_ring().order() == 2: - if self.nrows() <= 12: + if E.base_ring().order() == 2: + if E.nrows() <= 12: algorithm = 'naive' else: algorithm = 'elimination' - elif self.base_ring().degree() == 1: - if self.nrows() <= 4: + elif E.base_ring().degree() == 1: + if E.nrows() <= 4: algorithm = 'naive' else: algorithm = 'elimination' else: - if self.nrows() <= 2: + if E.nrows() <= 2: algorithm = 'naive' else: algorithm = 'elimination' if algorithm == 'naive': - return self._naive_krylov_basis(M, shifts, degree, output_row_indices) + return E._krylov_basis_naive(M, shifts, degree, output_rows) elif algorithm == 'elimination': - return self._elimination_krylov_basis(M, shifts, degree, output_row_indices) + return E._krylov_basis_elimination(M, shifts, degree, output_rows) else: raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, shifts=None, degree=None, output_coefficients=False, var='x'): + def krylov_kernel_basis(self, M, shifts=None, degree=None, var=None): r""" Return a shifted minimal krylov kernel basis for (``self``,``M``) in ``s``-Popov form with respect to a set of shifts ``s``. @@ -19840,11 +19802,11 @@ cdef class Matrix(Matrix1): and scalar multiplication is defined by p v := v p(``M``), ``self`` represents ``self.nrows()`` elements e_1, ..., e_m in V: - A linear interpolation basis is a set of tuples of length + A krylov kernel basis is a set of tuples of length ``self.nrows()`` in K[x] that form a basis for solutions to the equation ``p_1 e_1 + ... + p_n e_n = 0``. - See [Kai1980]_ + See Section 6.7 of [Kai1980]_ INPUT: @@ -19853,100 +19815,90 @@ cdef class Matrix(Matrix1): priority of rows. If ``None``, defaults to all zeroes. - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a suitable upper bound of ``self.ncols()`` is default. - - ``output_coefficients`` -- If True, outputs the matrix as a sparse - block matrix of coefficients. - - ``var`` -- variable name for the returned polynomial matrix + - ``var`` -- variable name for the returned m x m polynomial matrix. If + None (default), returns a m x (m*(deg+1)) matrix representing the + coefficient matrices of the output. OUTPUT: - - A self.ncols() * self.ncols() matrix whose rows form an interpolation - basis. + If ``var`` is ``None``, a ``self.nrows() * self.nrows()`` matrix ``P``. + If ``var`` is not ``None``, the corresponding + ``self.nrows() * (self.nrows() * (P.degree()+1))`` matrix ``C`` where + ``P[i,j][d] == C[i,m*d+j]``, such that + - ``P`` is ``s``-Popov + - the rows of ``C`` form a minimal basis for the kernel of + ``self.striped_krylov_matrix(M,P.degree())`` TESTS:: sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: M - [0 1 0] - [0 0 1] - [0 0 0] sage: degree = E.ncols() - sage: krylov_matrix = E.krylov_matrix(M) - sage: krylov_matrix - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: basis = E.krylov_kernel_basis(M) + sage: basis = E.krylov_kernel_basis(M,var='x') sage: basis [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] - sage: basis_coeff = E.krylov_kernel_basis(M,output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M) sage: basis_coeff [82 76 0 40 0 0 1 0 0] [13 57 0 3 1 0 0 0 0] [96 96 1 0 0 0 0 0 0] + sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) sage: basis.is_popov() True sage: basis.degree() <= E.ncols() True - sage: basis_coeff.augment(matrix.zero(R,E.nrows())) * krylov_matrix == matrix.zero(R,E.nrows()) + sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() + True + sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True - sage: shifts = [0,3,6] - sage: basis = E.krylov_kernel_basis(M,shifts=shifts) + sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') sage: basis [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] - sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts,output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) sage: basis_coeff [ 0 0 0 0 0 0 0 0 0 1 0 0] [70 1 0 72 0 0 60 0 0 0 0 0] [69 0 1 72 0 0 60 0 0 0 0 0] + sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True + sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() + True sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_basis(M,shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True - sage: shifts = [3,0,2] - sage: basis = E.krylov_kernel_basis(M,shifts=shifts) + sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') sage: basis [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] - sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts,output_coefficients=True) + sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) sage: basis_coeff [ 1 79 0 0 49 0 0 26 0 0 0 0] [ 0 0 0 0 0 0 0 0 0 0 1 0] [ 0 78 1 0 49 0 0 26 0 0 0 0] + sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True + sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() + True sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) True - sage: len(E.krylov_basis(M,shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True """ from sage.combinat.permutation import Permutation @@ -19954,47 +19906,51 @@ cdef class Matrix(Matrix1): from sage.modules.free_module_element import vector from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing + E = self + # INPUT VALIDATION + if not (E.base_ring() in _Fields and E.base_ring().is_exact()): + raise TypeError("krylov_kernel_basis: matrix entries must come from an exact field") + if not isinstance(M, Matrix): raise TypeError("krylov_kernel_basis: M is not a matrix") - if M.nrows() != self.ncols() or M.ncols() != self.ncols(): + if M.nrows() != E.ncols() or M.ncols() != E.ncols(): raise ValueError("krylov_kernel_basis: matrix M does not have correct dimensions.") - if M.base_ring() != self.base_ring(): - raise ValueError("krylov_kernel_basis: matrix M does not have same base ring as E.") + if M.base_ring() != E.base_ring(): + E, M = coercion_model.canonical_coercion(E, M) - if not isinstance(var, str): + if var is not None and not isinstance(var, str): raise TypeError("var is not a string") if degree is None: - degree = vector(ZZ, [self.ncols()] * self.nrows()) - if isinstance(degree, (int, sage.rings.integer.Integer)): - degree = vector(ZZ, [degree] * self.nrows()) - if isinstance(degree, (list, tuple)): - degree = vector(ZZ, degree) - if degree not in ZZ**self.nrows(): - raise TypeError(f"krylov_kernel_basis: degree must be an None, an integer, or an integer vector of length self.nrows().") - if self.nrows() > 0 and min(degree) < 0: + degree = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degree, (FreeModuleElement, list, tuple)): + degree = (ZZ**E.nrows())(degree) + else: + degree = (ZZ**E.nrows())([degree] * E.nrows()) + if E.nrows() > 0 and min(degree) < 0: raise ValueError(f"krylov_kernel_basis: degree must not contain a negative bound.") - if shifts is None or shifts == 0: - shifts = (ZZ**self.nrows()).zero() - if isinstance(shifts, (list, tuple)): - shifts = (ZZ**self.nrows())(shifts) - if shifts not in ZZ**self.nrows(): - raise ValueError(f"krylov_kernel_basis: shifts is not a Z-vector of length {self.nrows()}.") - - m = self.nrows() - sigma = self.ncols() + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"krylov_kernel_basis: shifts is not an integer vector of length {E.nrows()}.") - poly_ring = PolynomialRing(self.base_ring(),var) + m = E.nrows() + sigma = E.ncols() + base_ring = E.base_ring() # calculate krylov profile - pivot, row_profile, col_profile = self.krylov_basis(M, shifts, degree) + krylov_basis, row_profile = E.krylov_basis(M, shifts, degree) + col_profile = krylov_basis.pivots() if len(row_profile) == 0: - return matrix.identity(poly_ring,m) + ring = base_ring if var is None else PolynomialRing(base_ring, var) + return matrix.identity(ring, m) - c, d = zip(*(row for row in row_profile)) + c, d, _ = zip(*(row for row in row_profile)) # degree_c = 0 or max d[k] + 1 such that c[k] = c inv_c = [None]*m @@ -20006,8 +19962,8 @@ cdef class Matrix(Matrix1): T_absent_indices = [i for i in range(m) if inv_c[i] is None] T_present_indices = [inv_c[i] for i in range(m) if inv_c[i] is not None] - D_absent = self.matrix_from_rows_and_columns(T_absent_indices, col_profile) - D_present = (pivot.matrix_from_rows(T_present_indices)*M).matrix_from_columns(col_profile) + D_absent = E.matrix_from_rows_and_columns(T_absent_indices, col_profile) + D_present = (krylov_basis.matrix_from_rows(T_present_indices)*M).matrix_from_columns(col_profile) D_rows = [] idx_p = 0 idx_a = 0 @@ -20019,70 +19975,37 @@ cdef class Matrix(Matrix1): D_rows.append(D_present[idx_p]) idx_p += 1 - C = pivot.matrix_from_columns(col_profile) + C = krylov_basis.matrix_from_columns(col_profile) D = matrix(D_rows) relation = D*C.inverse() - if output_coefficients: - coefficients = matrix.zero(self.base_ring(),m,m*(max(degree_c, default=0)+1)) + if var is None: + # construct coefficient matrix + coefficients = matrix.zero(base_ring,m,m*(max(degree_c, default=0)+1)) + # add identity part of kernel basis for i in range(m): - coefficients[i,i+degree_c[i]*m] = self.base_ring().one() + coefficients[i,i+degree_c[i]*m] = base_ring.one() + # add relation part of kernel basis for col in range(relation.ncols()): for row in range(m): coefficients[row,c[col]+d[col]*m] -= relation[row,col] + return coefficients else: - # construct polynomial matrix - potentially costly at polynomial construction stage for extension fields + poly_ring = PolynomialRing(base_ring, var) + + # construct coefficient map coeffs_map = [[{} for _ in range(m)] for _ in range(m)] - # add identity part of basis + # add identity part of kernel basis for i in range(m): - coeffs_map[i][i][degree_c[i]] = self.base_ring().one() - # add relation part of basis + coeffs_map[i][i][degree_c[i]] = base_ring.one() + # add relation part of kernel basis for col in range(relation.ncols()): for row in range(m): - coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], self.base_ring().zero()) - relation[row,col] - # construct rows - basis_rows = [[None] * m for _ in range(m)] - if self.base_ring().degree() <= 1 or m*m*sigma < self.base_ring().order(): - # if base field is large, don't bother caching monomials - # or if polynomial construction is efficient (order = 1) - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = self.base_ring().zero() - else: - # construct a small polynomial (less costly), then shift - mindeg = min(coeffs_map[row][col].keys()) - maxdeg = max(coeffs_map[row][col].keys()) - entries = [self.base_ring().zero()] * (maxdeg-mindeg+1) - for deg, coeff in coeffs_map[row][col].items(): - entries[deg-mindeg] = coeff - basis_rows[row][col] = poly_ring(entries).shift(mindeg) - return matrix(poly_ring, basis_rows) - else: - # if base field is small, cache monomials to minimise number of constructions - monomial_cache = {} - for row in range(m): - for col in range(m): - if not coeffs_map[row][col]: - basis_rows[row][col] = poly_ring.zero() - elif len(coeffs_map[row][col]) == 1: - # monomial case - deg, coeff = coeffs_map[row][col].popitem() - if coeff not in monomial_cache: - monomial_cache[coeff] = poly_ring(coeff) - basis_rows[row][col] = monomial_cache[coeff].shift (deg) - else: - # general case - mindeg = min(coeffs_map[row][col].keys()) - poly = poly_ring.zero() - for deg, coeff in coeffs_map[row][col].items(): - if coeff not in monomial_cache: - monomial_cache[coeff] = poly_ring(coeff) - poly += monomial_cache[coeff].shift(deg-mindeg) - basis_rows[row][col] = poly.shift(mindeg) - # convert to matrix - return matrix(poly_ring,basis_rows) + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], base_ring.zero()) - relation[row,col] + + # convert to matrix (slow for extension fields) + return matrix(poly_ring, m, m, coeffs_map) # a limited number of access-only properties are provided for matrices @property From ce1fbd12c7e333cd3fe2339de97a2d18378e8e6c Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 21 Aug 2025 19:17:31 +0200 Subject: [PATCH 30/57] review changes --- src/sage/matrix/matrix2.pyx | 595 ++++++++++++++++++++++++------------ 1 file changed, 404 insertions(+), 191 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 3c2e8f692ac..870a097a098 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19111,7 +19111,87 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U - def krylov_matrix(self, M, shifts=None, degree=None): + def _krylov_row_coordinates(self, shifts, degrees, row_pairs=None): + r""" + Helper function for ``krylov_matrix`` and ``krylov_basis``. Returns a + sorted set of triplets corresponding to row coordinates, for a given set of + shifts and degrees. + + INPUT: + + - ``shifts`` -- an integer vector of length ``self.nrows()`` defining + the priority given to rows. + - ``degrees`` -- an integer vector of length ``self.nrows()`` defining + the maximum degree for rows. + - ``row_pairs`` -- the list of row coordinates to be sorted. If + ``None``, the default is + ``[(0,0),...,(self.nrows()-1,0),(0,1),...,(self.nrows()-1,1),...,(0,degrees[0]),...,(self.nrows()-1,degrees[self.nrows()-1])]``. + + OUTPUT: + + A list of triplets ``(c, d, i)`` sorted in ascending order by + ``shifts[c] + d`` then by ``c``. The entry ``i`` is the index of the + triplet after filtering but before sorting. + + EXAMPLES: + + sage: R = GF(97) + sage: E = matrix.zero(R,3) + sage: shifts = vector(ZZ,[0,0,0]) + sage: degrees = vector(ZZ,[3,3,3]) + sage: E._krylov_row_coordinates(shifts, degrees) + [(0, 0, 0), + (1, 0, 1), + (2, 0, 2), + (0, 1, 3), + (1, 1, 4), + (2, 1, 5), + (0, 2, 6), + (1, 2, 7), + (2, 2, 8), + (0, 3, 9), + (1, 3, 10), + (2, 3, 11)] + sage: shifts = vector(ZZ,[3,0,2]) + sage: degrees = vector(ZZ,[1,3,2]) + sage: E._krylov_row_coordinates(shifts,degrees) + [(1, 0, 1), + (1, 1, 4), + (1, 2, 6), + (2, 0, 2), + (0, 0, 0), + (1, 3, 8), + (2, 1, 5), + (0, 1, 3), + (2, 2, 7)] + sage: E._krylov_row_coordinates(shifts,degrees,[(2,2),(1,4),(1,2),(1,1),(0,2)]) + [(1, 1, 2), (1, 2, 1), (2, 2, 0)] + """ + # INPUT VALIDATION + if not isinstance(shifts, FreeModuleElement) or shifts not in ZZ**self.nrows(): + raise TypeError('_krylov_row_coordinates: shifts must be an integer vector of length sel.nrows().') + if not isinstance(degrees, FreeModuleElement) or degrees not in ZZ**self.nrows(): + raise TypeError('_krylov_row_coordinates: degrees must be an integer vector of length sel.nrows().') + if row_pairs is not None and (not isinstance(row_pairs, (list, tuple)) or any([not isinstance(x, tuple) or len(x) != 2 or any([not isinstance(y, (int, sage.rings.integer.Integer)) for y in x]) for x in row_pairs])): + raise TypeError('_krylov_row_coordinates: row_pairs must be a list or tuple of tuple pairs of integers.') + if any([d < 0 for d in degrees]): + raise ValueError('_krylov_row_coordinates: degrees cannot have negative entries.') + if row_pairs is not None and any([0 > r[0] or r[0] >= self.nrows() for r in row_pairs]): + raise ValueError('_krylov_row_coordinates: row_pairs cannot have out of bounds rows.') + + # (construct and) filter rows + blocks = max(degrees, default=0) + 1 + if row_pairs is None: + rows = [(i, j) for j in range(blocks) for i in range(self.nrows()) if j <= degrees[i]] + else: + rows = [row for row in row_pairs if row[1] <= degrees[row[0]]] + # index rows + rows = [(*row, i) for i, row in enumerate(rows)] + # sort rows + rows.sort(key=lambda x: (shifts[x[0]] + x[1], x[0])) + return rows + + def krylov_matrix(self, M, shifts=None, degrees=None): r""" Return the block matrix built from the rows of ``self`` and the matrix ``M``, with rows ordered according to a priority defined by @@ -19121,7 +19201,7 @@ cdef class Matrix(Matrix1): [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]_." The following example uses `E` to refer to ``self``, and `d` to refer - to ``degree`` (in the case where a single integer is provided). This is + to ``degrees`` (in the case where a single integer is provided). This is the case where ``shifts`` is `[0,...,0]``; another shift will correspond to some row permutation of this matrix. @@ -19147,7 +19227,7 @@ cdef class Matrix(Matrix1): ``self``. - ``shifts`` -- list of ``self.nrows()`` integers (optional), row priority shifts. If ``None``, defaults to all zeroes. - - ``degree`` -- the maximum power or list of ``self.nrows()`` maximum + - ``degrees`` -- the maximum power or list of ``self.nrows()`` maximum powers of ``M`` for each row in ``self`` in the output. If None, ``self.ncols()`` is chosen by default for all rows. @@ -19158,7 +19238,7 @@ cdef class Matrix(Matrix1): EXAMPLES:: - sage: R. = GF(97) + sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) sage: E [27 49 29] @@ -19208,6 +19288,13 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] + sage: E.krylov_matrix(M,[3,0,2],[0,2,1]) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 77 10] """ from sage.combinat.permutation import Permutation import math @@ -19217,9 +19304,6 @@ cdef class Matrix(Matrix1): E = self # INPUT VALIDATION - if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise TypeError("krylov_matrix: matrix entries must come from an exact field") - if not isinstance(M, Matrix): raise TypeError("krylov_matrix: M is not a matrix") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): @@ -19227,14 +19311,14 @@ cdef class Matrix(Matrix1): if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) - if degree is None: - degree = vector(ZZ, [E.ncols()] * E.nrows()) - elif isinstance(degree, (FreeModuleElement, list, tuple)): - degree = (ZZ**E.nrows())(degree) + if degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) else: - degree = (ZZ**E.nrows())([degree] * E.nrows()) - if E.nrows() > 0 and min(degree) < 0: - raise ValueError(f"krylov_matrix: degree must not contain a negative bound.") + degrees = (ZZ**E.nrows())([degrees] * E.nrows()) + if E.nrows() > 0 and min(degrees) < 0: + raise ValueError(f"krylov_matrix: degrees must not contain a negative bound.") if shifts is None: shifts = (ZZ**E.nrows()).zero() @@ -19245,17 +19329,6 @@ cdef class Matrix(Matrix1): m = E.nrows() - # define priority and indexing functions - # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shifts[c] + d - index = lambda i : (i%m,i//m) - - # deduce permutation by sorting priorities - # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_triplets = [[priority(*index(i)),index(i)] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]] - priority_triplets = sorted([[t[0],t[1],i] for i,t in enumerate(priority_triplets)]) - priority_permutation = Permutation([t[2]+1 for t in priority_triplets]) - # store 2 blocks for the Krylov matrix blocks = [E] @@ -19264,21 +19337,24 @@ cdef class Matrix(Matrix1): # for i from 0 to degree (inclusive), merge existing blocks, add E*M^i M_i = None - for i in range(math.ceil(math.log(max(degree, default=0) + 1, 2))): + for i in range(math.ceil(math.log(max(degrees, default=0) + 1, 2))): if M_i is None: M_i = M else: M_i = M_i * M_i blocks = [blocks[0].stack(blocks[1],subdivide=False)] new_indices = [(row[0], row[1] + 2**i) for row in indices] - blocks.append((blocks[0].matrix_from_rows([i for i,x in enumerate(new_indices) if x[1] <= degree[x[0]]]) * M_i)) - indices.extend([x for x in new_indices if x[1] <= degree[x[0]]]) + blocks.append((blocks[0].matrix_from_rows([i for i,x in enumerate(new_indices) if x[1] <= degrees[x[0]]]) * M_i)) + indices.extend([x for x in new_indices if x[1] <= degrees[x[0]]]) # return block matrix permuted according to shifts krylov = blocks[0].stack(blocks[1],subdivide=False) if len(blocks) > 1 else blocks[0] - return krylov.with_permuted_rows(priority_permutation) - def _krylov_basis_naive(self, M, shifts, degree, output_rows): + permutation = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts, degrees)]) + + return krylov.with_permuted_rows(permutation) + + def _krylov_basis_naive(self, M, shifts, degrees, output_rows): r""" Compute the rank profile (row and column) of the Krylov matrix built from ``self`` and matrix ``M``. @@ -19293,13 +19369,12 @@ cdef class Matrix(Matrix1): - ``M`` -- square matrix used in the Krylov construction. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. - - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether the output row indices - are pairs of row indices in ``self`` and degrees of ``M`` (False) or - row indices in the Krylov matrix (True). + - ``output_rows`` -- determines whether the indices of the rows + returned are also provided. OUTPUT: @@ -19311,7 +19386,7 @@ cdef class Matrix(Matrix1): ``c`` is the corresponding row in ``self`` and ``d`` is the corresponding power of ``M``. - TESTS:: + EXAMPLES:: sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) @@ -19324,7 +19399,7 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: degree = 3 + sage: degrees = 3 sage: K = E.krylov_matrix(M) sage: K [27 49 29] @@ -19345,35 +19420,35 @@ cdef class Matrix(Matrix1): [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] + sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] sage: rows - [(1, 0, 0), - (2, 1, 0), - (3, 2, 0), - (4, 0, 1), - (5, 1, 1), - (6, 2, 1), - (7, 0, 2), - (8, 1, 2), - (9, 2, 2), - (10, 0, 3), - (11, 1, 3), - (12, 2, 3)] + [(0, 0, 0), + (1, 0, 1), + (2, 0, 2), + (0, 1, 3), + (1, 1, 4), + (2, 1, 5), + (0, 2, 6), + (1, 2, 7), + (2, 2, 8), + (0, 3, 9), + (1, 3, 10), + (2, 3, 11)] sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) sage: rows - [(1, 0, 0), - (4, 0, 1), - (7, 0, 2), - (10, 0, 3), - (2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (11, 1, 3), - (3, 2, 0), - (6, 2, 1), - (9, 2, 2), - (12, 2, 3)] + [(0, 0, 0), + (0, 1, 3), + (0, 2, 6), + (0, 3, 9), + (1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (1, 3, 10), + (2, 0, 2), + (2, 1, 5), + (2, 2, 8), + (2, 3, 11)] sage: E.krylov_matrix(M,shifts=shifts) [27 49 29] [ 0 27 49] @@ -19394,20 +19469,20 @@ cdef class Matrix(Matrix1): [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) sage: rows - [(2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (3, 2, 0), - (1, 0, 0), - (11, 1, 3), - (6, 2, 1), - (4, 0, 1), - (9, 2, 2), - (7, 0, 2), - (12, 2, 3), - (10, 0, 3)] + [(1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (2, 0, 2), + (0, 0, 0), + (1, 3, 10), + (2, 1, 5), + (0, 1, 3), + (2, 2, 8), + (0, 2, 6), + (2, 3, 11), + (0, 3, 9)] sage: E.krylov_matrix(M,shifts=shifts) [50 58 0] [ 0 50 58] @@ -19432,7 +19507,7 @@ cdef class Matrix(Matrix1): m = self.nrows() - K = self.krylov_matrix(M,shifts,degree) + K = self.krylov_matrix(M,shifts,degrees) # calculate first independent rows of K row_profile = K.pivot_rows() @@ -19443,20 +19518,13 @@ cdef class Matrix(Matrix1): if not output_rows: return pivot - # define priority and indexing functions - # index: [0,1,...,m-1,m,...,2m-1,...,m*degree-1] -> [(0,0),(1,0),...,(m-1,0),(0,1),...,(m-1,1),...,(m-1,degree)] - priority = lambda c,d : shifts[c] + d - index = lambda i : (i%m,i//m) + row_coordinates = self._krylov_row_coordinates(shifts, degrees) - # deduce permutation by sorting priorities - # rows should be ordered by ascending priority, then by associated row number in E (i%m) - priority_pairs = sorted([[priority(*index(i)),index(i),i] for i in range(m*(max(degree, default=0)+1)) if index(i)[1] <= degree[index(i)[0]]]) - - row_profile = tuple([(*priority_pairs[i][1], i) for i in row_profile]) + row_profile = tuple([(*row_coordinates[i][:2], i) for i in row_profile]) return pivot, row_profile - def _krylov_basis_elimination(self, M, shifts, degree, output_rows): + def _krylov_basis_elimination(self, M, shifts, degrees, output_rows): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19471,13 +19539,12 @@ cdef class Matrix(Matrix1): - ``M`` -- square matrix used in the Krylov construction. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. - - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether the output row indices - are pairs of row indices in ``self`` and degrees of ``M`` (False) or - row indices in the Krylov matrix (True). + - ``output_rows`` -- determines whether the indices of the rows + returned are also provided. OUTPUT: @@ -19489,7 +19556,7 @@ cdef class Matrix(Matrix1): ``c`` is the corresponding row in ``self`` and ``d`` is the corresponding power of ``M``. - TESTS:: + EXAMPLES:: sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) @@ -19502,7 +19569,7 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: degree = 3 + sage: degrees = 3 sage: K = E.krylov_matrix(M) sage: K [27 49 29] @@ -19523,35 +19590,35 @@ cdef class Matrix(Matrix1): [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: rows = [(i+j*E.nrows()+1,i,j) for j in range(degree+1) for i in range(E.nrows())] + sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] sage: rows - [(1, 0, 0), - (2, 1, 0), - (3, 2, 0), - (4, 0, 1), - (5, 1, 1), - (6, 2, 1), - (7, 0, 2), - (8, 1, 2), - (9, 2, 2), - (10, 0, 3), - (11, 1, 3), - (12, 2, 3)] + [(0, 0, 0), + (1, 0, 1), + (2, 0, 2), + (0, 1, 3), + (1, 1, 4), + (2, 1, 5), + (0, 2, 6), + (1, 2, 7), + (2, 2, 8), + (0, 3, 9), + (1, 3, 10), + (2, 3, 11)] sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) sage: rows - [(1, 0, 0), - (4, 0, 1), - (7, 0, 2), - (10, 0, 3), - (2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (11, 1, 3), - (3, 2, 0), - (6, 2, 1), - (9, 2, 2), - (12, 2, 3)] + [(0, 0, 0), + (0, 1, 3), + (0, 2, 6), + (0, 3, 9), + (1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (1, 3, 10), + (2, 0, 2), + (2, 1, 5), + (2, 2, 8), + (2, 3, 11)] sage: E.krylov_matrix(M,shifts=shifts) [27 49 29] [ 0 27 49] @@ -19572,20 +19639,20 @@ cdef class Matrix(Matrix1): [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[2] + shifts[x[1]],x[1])) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) sage: rows - [(2, 1, 0), - (5, 1, 1), - (8, 1, 2), - (3, 2, 0), - (1, 0, 0), - (11, 1, 3), - (6, 2, 1), - (4, 0, 1), - (9, 2, 2), - (7, 0, 2), - (12, 2, 3), - (10, 0, 3)] + [(1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (2, 0, 2), + (0, 0, 0), + (1, 3, 10), + (2, 1, 5), + (0, 1, 3), + (2, 2, 8), + (0, 2, 6), + (2, 3, 11), + (0, 3, 9)] sage: E.krylov_matrix(M,shifts=shifts) [50 58 0] [ 0 50 58] @@ -19617,7 +19684,8 @@ cdef class Matrix(Matrix1): return (self, ()) if output_rows else self # calculate row profile of self, with shifts applied - self_permutation = Permutation(sorted([i+1 for i in range(m)],key=lambda x:(shifts[x-1],x-1))) + self_coordinates = self._krylov_row_coordinates(shifts, degrees, [(i,0) for i in range(m)]) + self_permutation = Permutation([x[2] + 1 for x in self_coordinates]) self_permuted = self.with_permuted_rows(self_permutation) row_profile_self_permuted = self_permuted.pivot_rows() @@ -19630,16 +19698,16 @@ cdef class Matrix(Matrix1): r = len(row_profile_self) if r == 0: - return (self.matrix_from_rows([]),()) if output_rows else self.matrix_from_rows([]) + return (self.matrix_from_rows([]), ()) if output_rows else self.matrix_from_rows([]) R = self_permuted.matrix_from_rows(row_profile_self_permuted) M_L = None - for l in range(math.ceil(math.log(max(degree, default=0) + 1, 2))): + for l in range(math.ceil(math.log(max(degrees, default=0) + 1, 2))): L = pow(2, l) # adding 2^l to each degree - row_extension = [(x[0], x[1] + L) for x in row_profile_self if x[1] + L <= degree[x[0]]] + row_extension = [(x[0], x[1] + L) for x in row_profile_self if x[1] + L <= degrees[x[0]]] if len(row_extension) == 0: break @@ -19647,7 +19715,8 @@ cdef class Matrix(Matrix1): k = row_profile_exhausted + row_profile_self + row_extension # calculate sorting permutation, sort k by (shifts[c]+d, c) - k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shifts[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + k_coordinates = self._krylov_row_coordinates(shifts, degrees, k) + k_perm = Permutation([x[2] + 1 for x in k_coordinates]) # fast calculation of rows formed by indices in k if M_L is None: @@ -19655,7 +19724,7 @@ cdef class Matrix(Matrix1): else: M_L = M_L * M_L - R = matrix.block([[exhausted],[R],[(R*M_L).matrix_from_rows([i for i in range(len(row_profile_self)) if row_profile_self[i][1] + L <= degree[row_profile_self[i][0]]])]], subdivide=False) + R = matrix.block([[exhausted],[R],[(R*M_L).matrix_from_rows([i for i in range(len(row_profile_self)) if row_profile_self[i][1] + L <= degrees[row_profile_self[i][0]]])]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) R.permute_rows(k_perm) @@ -19663,7 +19732,7 @@ cdef class Matrix(Matrix1): row_profile_R = R.pivot_rows() r = len(row_profile_R) - if r == sigma and l < math.ceil(math.log(max(degree, default=0) + 1, 2)) - 1: + if r == sigma and l < math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: tail = list(range(row_profile_R[-1]+1,R.nrows())) excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) @@ -19676,7 +19745,7 @@ cdef class Matrix(Matrix1): exhausted = R.matrix_from_rows(xmi) R = R.matrix_from_rows(imi) else: - if l == math.ceil(math.log(max(degree, default=0) + 1, 2)) - 1: + if l == math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: row_profile_exhausted = [] exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_R] @@ -19688,7 +19757,9 @@ cdef class Matrix(Matrix1): elif exhausted.nrows() != 0: k = row_profile_exhausted + row_profile_self R = exhausted.stack(R) - k_perm = Permutation(sorted([i+1 for i in range(len(k))],key=lambda x: (shifts[k[x-1][0]] + k[x-1][1],k[x-1][0]))) + + k_coordinates = self._krylov_row_coordinates(shifts, degrees, k) + k_perm = Permutation([x[2] + 1 for x in k_coordinates]) R.permute_rows(k_perm) row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] @@ -19697,12 +19768,12 @@ cdef class Matrix(Matrix1): return R # convert c,d to actual position in striped Krylov matrix - phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degree[i] + 1) for i in range(m)) - row_profile = tuple([(*row,phi(*row)) for row in row_profile_self]) + phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degrees[i] + 1) for i in range(m)) + row_profile = tuple([(*row, phi(*row)) for row in row_profile_self]) return R, row_profile - def krylov_basis(self, M, shifts=None, degree=None, output_rows=True, algorithm=None): + def krylov_basis(self, M, shifts=None, degrees=None, output_rows=True, algorithm=None): r""" Compute the rank profile (row and column) of the block Krylov matrix built from ``self`` and matrix ``M``. @@ -19718,13 +19789,12 @@ cdef class Matrix(Matrix1): - ``M`` -- square matrix used in the Krylov construction. - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority row shifts. If ``None``, defaults to all zeroes. - - ``degree`` -- an upper bound or list of ``self.nrows()`` upper bounds + - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds on the power of ``M`` for each row in ``self`` in the ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether ``row_profile`` is - returned as pairs of row indices in ``self`` and degrees of ``M`` - (``False``), or row indices in the Krylov matrix (``True``). + - ``output_rows`` -- determines whether the indices of the rows + returned are also provided. - ``algorithm`` -- either 'naive', 'elimination', or None (let Sage choose). @@ -19737,6 +19807,123 @@ cdef class Matrix(Matrix1): basis where ``i`` is the row index of the row in the krylov matrix, ``c`` is the corresponding row in ``self`` and ``d`` is the corresponding power of ``M``. + + EXAMPLES:: + + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M + [0 1 0] + [0 0 1] + [0 0 0] + sage: degrees = 3 + sage: K = E.krylov_matrix(M) + sage: K + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 27 49] + [ 0 50 58] + [ 0 77 10] + [ 0 0 27] + [ 0 0 50] + [ 0 0 77] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_basis(M,output_rows=True,algorithm='elimination') + ( + [27 49 29] + [50 58 0] + [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) + ) + sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] + sage: rows + [(0, 0, 0), + (1, 0, 1), + (2, 0, 2), + (0, 1, 3), + (1, 1, 4), + (2, 1, 5), + (0, 2, 6), + (1, 2, 7), + (2, 2, 8), + (0, 3, 9), + (1, 3, 10), + (2, 3, 11)] + sage: shifts = [0,3,6] + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) + sage: rows + [(0, 0, 0), + (0, 1, 3), + (0, 2, 6), + (0, 3, 9), + (1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (1, 3, 10), + (2, 0, 2), + (2, 1, 5), + (2, 2, 8), + (2, 3, 11)] + sage: E.krylov_matrix(M,shifts=shifts) + [27 49 29] + [ 0 27 49] + [ 0 0 27] + [ 0 0 0] + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [ 0 0 0] + [77 10 29] + [ 0 77 10] + [ 0 0 77] + [ 0 0 0] + sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + ( + [27 49 29] + [ 0 27 49] + [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) + ) + sage: shifts = [3,0,2] + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) + sage: rows + [(1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (2, 0, 2), + (0, 0, 0), + (1, 3, 10), + (2, 1, 5), + (0, 1, 3), + (2, 2, 8), + (0, 2, 6), + (2, 3, 11), + (0, 3, 9)] + sage: E.krylov_matrix(M,shifts=shifts) + [50 58 0] + [ 0 50 58] + [ 0 0 50] + [77 10 29] + [27 49 29] + [ 0 0 0] + [ 0 77 10] + [ 0 27 49] + [ 0 0 77] + [ 0 0 27] + [ 0 0 0] + [ 0 0 0] + sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + ( + [50 58 0] + [ 0 50 58] + [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) + ) """ from sage.modules.free_module_element import vector @@ -19753,14 +19940,14 @@ cdef class Matrix(Matrix1): if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) - if degree is None: - degree = vector(ZZ, [E.ncols()] * E.nrows()) - elif isinstance(degree, (FreeModuleElement, list, tuple)): - degree = (ZZ**E.nrows())(degree) + if degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) else: - degree = (ZZ**E.nrows())([degree] * E.nrows()) - if E.nrows() > 0 and min(degree) < 0: - raise ValueError(f"krylov_basis: degree must not contain a negative bound.") + degrees = (ZZ**E.nrows())([degrees] * E.nrows()) + if E.nrows() > 0 and min(degrees) < 0: + raise ValueError(f"krylov_basis: degrees must not contain a negative bound.") if shifts is None: shifts = (ZZ**E.nrows()).zero() @@ -19786,13 +19973,13 @@ cdef class Matrix(Matrix1): else: algorithm = 'elimination' if algorithm == 'naive': - return E._krylov_basis_naive(M, shifts, degree, output_rows) + return E._krylov_basis_naive(M, shifts, degrees, output_rows) elif algorithm == 'elimination': - return E._krylov_basis_elimination(M, shifts, degree, output_rows) + return E._krylov_basis_elimination(M, shifts, degrees, output_rows) else: raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, shifts=None, degree=None, var=None): + def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): r""" Return a shifted minimal krylov kernel basis for (``self``,``M``) in ``s``-Popov form with respect to a set of shifts ``s``. @@ -19808,12 +19995,17 @@ cdef class Matrix(Matrix1): See Section 6.7 of [Kai1980]_ + .. WARNING:: + + If the degree bounds are not true upper bounds, no guarantees are + made on the correctness of the output. + INPUT: - ``M`` -- square matrix for Krylov construction. - ``shifts`` -- list of self.nrows() integers (optional): controls priority of rows. If ``None``, defaults to all zeroes. - - ``degree`` -- upper bound on degree of minpoly(`M`). If None, a + - ``degrees`` -- upper bound on degree of minpoly(`M`). If None, a suitable upper bound of ``self.ncols()`` is default. - ``var`` -- variable name for the returned m x m polynomial matrix. If None (default), returns a m x (m*(deg+1)) matrix representing the @@ -19829,23 +20021,56 @@ cdef class Matrix(Matrix1): - the rows of ``C`` form a minimal basis for the kernel of ``self.striped_krylov_matrix(M,P.degree())`` - TESTS:: + EXAMPLES:: sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E + [27 49 29] + [50 58 0] + [77 10 29] sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: degree = E.ncols() - sage: basis = E.krylov_kernel_basis(M,var='x') - sage: basis + sage: M + [0 1 0] + [0 0 1] + [0 0 0] + sage: E.krylov_kernel_basis(M,var='x') [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] - sage: basis_coeff = E.krylov_kernel_basis(M) - sage: basis_coeff + sage: E.krylov_kernel_basis(M) [82 76 0 40 0 0 1 0 0] [13 57 0 3 1 0 0 0 0] [96 96 1 0 0 0 0 0 0] - sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) + sage: shifts = [0,3,6] + sage: E.krylov_kernel_basis(M,shifts=shifts,var='x') + [ x^3 0 0] + [60*x^2 + 72*x + 70 1 0] + [60*x^2 + 72*x + 69 0 1] + sage: E.krylov_kernel_basis(M,shifts=shifts) + [ 0 0 0 0 0 0 0 0 0 1 0 0] + [70 1 0 72 0 0 60 0 0 0 0 0] + [69 0 1 72 0 0 60 0 0 0 0 0] + sage: shifts = [3,0,2] + sage: E.krylov_kernel_basis(M,shifts=shifts,var='x') + [ 1 26*x^2 + 49*x + 79 0] + [ 0 x^3 0] + [ 0 26*x^2 + 49*x + 78 1] + sage: E.krylov_kernel_basis(M,shifts=shifts) + [ 1 79 0 0 49 0 0 26 0 0 0 0] + [ 0 0 0 0 0 0 0 0 0 0 1 0] + [ 0 78 1 0 49 0 0 26 0 0 0 0] + + TESTS:: + + sage: R = GF(97) + sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: basis = E.krylov_kernel_basis(M,var='x') + sage: basis_coeff = E.krylov_kernel_basis(M) + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + True + sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) sage: basis.is_popov() True sage: basis.degree() <= E.ncols() @@ -19858,16 +20083,10 @@ cdef class Matrix(Matrix1): True sage: shifts = [0,3,6] sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') - sage: basis - [ x^3 0 0] - [60*x^2 + 72*x + 70 1 0] - [60*x^2 + 72*x + 69 0 1] sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) - sage: basis_coeff - [ 0 0 0 0 0 0 0 0 0 1 0 0] - [70 1 0 72 0 0 60 0 0 0 0 0] - [69 0 1 72 0 0 60 0 0 0 0 0] - sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + True + sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() @@ -19880,16 +20099,10 @@ cdef class Matrix(Matrix1): True sage: shifts = [3,0,2] sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') - sage: basis - [ 1 26*x^2 + 49*x + 79 0] - [ 0 x^3 0] - [ 0 26*x^2 + 49*x + 78 1] sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) - sage: basis_coeff - [ 1 79 0 0 49 0 0 26 0 0 0 0] - [ 0 0 0 0 0 0 0 0 0 0 1 0] - [ 0 78 1 0 49 0 0 26 0 0 0 0] - sage: krylov_matrix = E.krylov_matrix(M,degree=max(basis.degree(),0)) + sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + True + sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() @@ -19922,14 +20135,14 @@ cdef class Matrix(Matrix1): if var is not None and not isinstance(var, str): raise TypeError("var is not a string") - if degree is None: - degree = vector(ZZ, [E.ncols()] * E.nrows()) - elif isinstance(degree, (FreeModuleElement, list, tuple)): - degree = (ZZ**E.nrows())(degree) + if degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) else: - degree = (ZZ**E.nrows())([degree] * E.nrows()) - if E.nrows() > 0 and min(degree) < 0: - raise ValueError(f"krylov_kernel_basis: degree must not contain a negative bound.") + degrees = (ZZ**E.nrows())([degrees] * E.nrows()) + if E.nrows() > 0 and min(degrees) < 0: + raise ValueError(f"krylov_kernel_basis: degrees must not contain a negative bound.") if shifts is None: shifts = (ZZ**E.nrows()).zero() @@ -19943,7 +20156,7 @@ cdef class Matrix(Matrix1): base_ring = E.base_ring() # calculate krylov profile - krylov_basis, row_profile = E.krylov_basis(M, shifts, degree) + krylov_basis, row_profile = E.krylov_basis(M, shifts, degrees) col_profile = krylov_basis.pivots() if len(row_profile) == 0: From 01b706b19c9cca54431d81397a3a98c080c647ab Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 25 Aug 2025 14:20:39 +0200 Subject: [PATCH 31/57] changes --- src/sage/matrix/matrix2.pyx | 65 ++++++++++--------------------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 870a097a098..80993461ddb 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19167,18 +19167,6 @@ cdef class Matrix(Matrix1): sage: E._krylov_row_coordinates(shifts,degrees,[(2,2),(1,4),(1,2),(1,1),(0,2)]) [(1, 1, 2), (1, 2, 1), (2, 2, 0)] """ - # INPUT VALIDATION - if not isinstance(shifts, FreeModuleElement) or shifts not in ZZ**self.nrows(): - raise TypeError('_krylov_row_coordinates: shifts must be an integer vector of length sel.nrows().') - if not isinstance(degrees, FreeModuleElement) or degrees not in ZZ**self.nrows(): - raise TypeError('_krylov_row_coordinates: degrees must be an integer vector of length sel.nrows().') - if row_pairs is not None and (not isinstance(row_pairs, (list, tuple)) or any([not isinstance(x, tuple) or len(x) != 2 or any([not isinstance(y, (int, sage.rings.integer.Integer)) for y in x]) for x in row_pairs])): - raise TypeError('_krylov_row_coordinates: row_pairs must be a list or tuple of tuple pairs of integers.') - if any([d < 0 for d in degrees]): - raise ValueError('_krylov_row_coordinates: degrees cannot have negative entries.') - if row_pairs is not None and any([0 > r[0] or r[0] >= self.nrows() for r in row_pairs]): - raise ValueError('_krylov_row_coordinates: row_pairs cannot have out of bounds rows.') - # (construct and) filter rows blocks = max(degrees, default=0) + 1 if row_pairs is None: @@ -19356,8 +19344,7 @@ cdef class Matrix(Matrix1): def _krylov_basis_naive(self, M, shifts, degrees, output_rows): r""" - Compute the rank profile (row and column) of the Krylov matrix - built from ``self`` and matrix ``M``. + Refer to `krylov_basis`. .. WARNING:: @@ -19526,36 +19513,13 @@ cdef class Matrix(Matrix1): def _krylov_basis_elimination(self, M, shifts, degrees, output_rows): r""" - Compute the rank profile (row and column) of the block Krylov matrix - built from ``self`` and matrix ``M``. + Refer to `krylov_basis`. .. WARNING:: If the degree bounds are not true upper bounds, no guarantees are made on the value of the output. - INPUT: - - - ``M`` -- square matrix used in the Krylov construction. - - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority - row shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds - on the power of ``M`` for each row in ``self`` in the - ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper - bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether the indices of the rows - returned are also provided. - - OUTPUT: - - - krylov_basis: the submatrix of the krylov matrix of ``self`` and - ``M`` formed by its lexicographically-first independent rows. - - row_profile (returned if ``output_rows`` is ``True``): list of the - ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov - basis where ``i`` is the row index of the row in the krylov matrix, - ``c`` is the corresponding row in ``self`` and ``d`` is the - corresponding power of ``M``. - EXAMPLES:: sage: R = GF(97) @@ -19715,8 +19679,8 @@ cdef class Matrix(Matrix1): k = row_profile_exhausted + row_profile_self + row_extension # calculate sorting permutation, sort k by (shifts[c]+d, c) - k_coordinates = self._krylov_row_coordinates(shifts, degrees, k) - k_perm = Permutation([x[2] + 1 for x in k_coordinates]) + k_rows = [x[2] for x in self._krylov_row_coordinates(shifts, degrees, k)] + k_perm = Permutation([x + 1 for x in k_rows]) # fast calculation of rows formed by indices in k if M_L is None: @@ -19724,7 +19688,9 @@ cdef class Matrix(Matrix1): else: M_L = M_L * M_L - R = matrix.block([[exhausted],[R],[(R*M_L).matrix_from_rows([i for i in range(len(row_profile_self)) if row_profile_self[i][1] + L <= degrees[row_profile_self[i][0]]])]], subdivide=False) + rows = [i for i, x in enumerate(row_profile_self) if x[1] + L <= degrees[x[0]]] + S = (R*M_L).matrix_from_rows(rows) + R = matrix.block([[exhausted],[R],[S]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) R.permute_rows(k_perm) @@ -19782,7 +19748,10 @@ cdef class Matrix(Matrix1): If the degree bounds are not true upper bounds, no guarantees are made on the value of the output, including whether it is consistent - between different algorithms. + between different algorithms. An upper bound `\delta_i` on the row + `i` is such that for every `j > \delta_i`, `E_{i,*}M^j` is + dependent on the rows before it in an "infinite" Krylov matrix. + `self.ncols()` is known to always be an upper bound. INPUT: @@ -19800,9 +19769,9 @@ cdef class Matrix(Matrix1): OUTPUT: - - krylov_basis: the submatrix of the krylov matrix of ``self`` and - ``M`` formed by its lexicographically-first independent rows. - - row_profile (returned if ``output_rows`` is ``True``): list of the + - ``krylov_basis``: the submatrix of the krylov matrix of ``self`` and + ``M`` formed by its first independent rows. + - ``row_profile`` (returned if ``output_rows`` is ``True``): list of the ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov basis where ``i`` is the row index of the row in the krylov matrix, ``c`` is the corresponding row in ``self`` and ``d`` is the @@ -19931,7 +19900,7 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise TypeError("krylov_basis: matrix entries must come from an exact field") + raise NotImplementedError("krylov_basis: matrix entries must come from an exact field") if not isinstance(M, Matrix): raise TypeError("krylov_basis: M is not a matrix") @@ -19985,7 +19954,7 @@ cdef class Matrix(Matrix1): ``s``-Popov form with respect to a set of shifts ``s``. Given a K[x]-module V defined by multiplication matrix ``M``, i.e. - elements are vectors of length n in K, scalars are polynomials in K[x], + elements are vectors of length n in K, scalars are polynomials in `K[x]`, and scalar multiplication is defined by p v := v p(``M``), ``self`` represents ``self.nrows()`` elements e_1, ..., e_m in V: @@ -20123,7 +20092,7 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise TypeError("krylov_kernel_basis: matrix entries must come from an exact field") + raise NotImplementedError("krylov_kernel_basis: matrix entries must come from an exact field") if not isinstance(M, Matrix): raise TypeError("krylov_kernel_basis: M is not a matrix") From 60b0132db88d4dc0a0317a85290fc5c5523a0cb0 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Mon, 25 Aug 2025 14:53:49 +0200 Subject: [PATCH 32/57] replace perm(i+1)-1 with rows[i] --- src/sage/matrix/matrix2.pyx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 80993461ddb..7485ded7e47 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19648,12 +19648,12 @@ cdef class Matrix(Matrix1): return (self, ()) if output_rows else self # calculate row profile of self, with shifts applied - self_coordinates = self._krylov_row_coordinates(shifts, degrees, [(i,0) for i in range(m)]) - self_permutation = Permutation([x[2] + 1 for x in self_coordinates]) + self_rows = [x[2] for x in self._krylov_row_coordinates(shifts, degrees, [(i,0) for i in range(m)])] + self_permutation = Permutation([x + 1 for x in self_rows]) self_permuted = self.with_permuted_rows(self_permutation) row_profile_self_permuted = self_permuted.pivot_rows() - row_profile_self = [(self_permutation(i+1)-1,0) for i in row_profile_self_permuted] + row_profile_self = [(self_rows[i], 0) for i in row_profile_self_permuted] exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_exhausted = [] @@ -19700,13 +19700,13 @@ cdef class Matrix(Matrix1): if r == sigma and l < math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: tail = list(range(row_profile_R[-1]+1,R.nrows())) - excluded_rows.update(set([k[k_perm(i+1)-1][0] for i in tail if i < len(k)])) + excluded_rows.update(set([k[k_rows[i]][0] for i in tail if i < len(k)])) - xmi = [i for i in row_profile_R if k[k_perm(i+1)-1][0] in excluded_rows] - imi = [i for i in row_profile_R if k[k_perm(i+1)-1][0] not in excluded_rows] + xmi = [i for i in row_profile_R if k[k_rows[i]][0] in excluded_rows] + imi = [i for i in row_profile_R if k[k_rows[i]][0] not in excluded_rows] - row_profile_exhausted = [k[k_perm(i+1)-1] for i in xmi] - row_profile_self = [k[k_perm(i+1)-1] for i in imi] + row_profile_exhausted = [k[k_rows[i]] for i in xmi] + row_profile_self = [k[k_rows[i]] for i in imi] exhausted = R.matrix_from_rows(xmi) R = R.matrix_from_rows(imi) @@ -19714,7 +19714,7 @@ cdef class Matrix(Matrix1): if l == math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: row_profile_exhausted = [] exhausted = matrix.zero(self.base_ring(), 0, sigma) - row_profile_self = [k[k_perm(i+1)-1] for i in row_profile_R] + row_profile_self = [k[k_rows[i]] for i in row_profile_R] # calculate new R for return value or next loop R = R.matrix_from_rows(row_profile_R) if R.nrows() == 0: @@ -19724,11 +19724,11 @@ cdef class Matrix(Matrix1): k = row_profile_exhausted + row_profile_self R = exhausted.stack(R) - k_coordinates = self._krylov_row_coordinates(shifts, degrees, k) - k_perm = Permutation([x[2] + 1 for x in k_coordinates]) + k_rows = [x[2] for x in self._krylov_row_coordinates(shifts, degrees, k)] + k_perm = Permutation([x + 1 for x in k_rows]) R.permute_rows(k_perm) - row_profile_self = [k[k_perm(i+1)-1] for i in range(len(k))] + row_profile_self = [k[k_rows[i]] for i in range(len(k))] if not output_rows: return R From f9e2455d478c62c66ddb4e65b99656fc9c0292ba Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 12:48:56 +0200 Subject: [PATCH 33/57] improve documentation for krylov_matrix --- src/doc/en/reference/references/index.rst | 17 ++- src/sage/matrix/matrix2.pyx | 174 ++++++++++++---------- 2 files changed, 112 insertions(+), 79 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 67ea592dc4b..beca855348d 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1011,6 +1011,11 @@ REFERENCES: Anal. Appl. 15 (1994) 804-823. :doi:`10.1137/S0895479892230031` +.. [BL2000] Bernhard Beckermann, George Labahn. *Fraction-Free Computation of + Matrix Rational Interpolants and Matrix GCDs*. SIAM J. Matrix + Anal. Appl. 22 (2000) 114-144. + :doi:`10.1137/S0895479897326912` + .. [BL1995] \W. Bosma, H.W. Lenstra: *Complete Systems of Two Addition Laws for Elliptic Curves*. Journal of Number Theory, volume 53, issue 2, pages 229-240. 1995. @@ -3808,9 +3813,15 @@ REFERENCES: http://code.google.com/p/graph-theory-algorithms-book/ .. [JNSV2016] Claude-Pierre Jeannerod, Vincent Neiger, Eric Schost, and Gilles - Villard. Fast Computation of Minimal Interpolation Bases in Popov - Form for Arbitrary Shifts. In Proceedings ISSAC 2016 (pages - 295-302). :doi:`10.1145/2930889.2930928` + Villard. *Fast Computation of Minimal Interpolation Bases in + Popov Form for Arbitrary Shifts*. In Proceedings ISSAC 2016 + (pages 295-302). + :doi:`10.1145/2930889.2930928` + +.. [JNSV2017] Claude-Pierre Jeannerod, Vincent Neiger, Eric Schost, and Gilles + Villard. *Computing Minimal Interpolation Bases*. J. Symb. + Comput. 83, 2017 (pp 272--314). + :doi:`10.1016/j.jsc.2016.11.015` .. [Joh1990] \D.L. Johnson. *Presentations of Groups*. Cambridge University Press. (1990). diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 7485ded7e47..2750b20cda2 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -13203,6 +13203,11 @@ cdef class Matrix(Matrix1): list is the column index of the pivot column containing its lone 1 in row ``i``. + .. SEEALSO:: + + :meth:`krylov_matrix` and :meth:`krylov_basis`, which compute + Krylov iterates and Krylov bases for several vectors at a time + ALGORITHM: This could be called an "online echelon form" routine. As each @@ -13332,6 +13337,11 @@ cdef class Matrix(Matrix1): For less convenient, but more flexible output, see the helper method "_cyclic_subspace" in this module. + .. SEEALSO:: + + :meth:`krylov_matrix` and :meth:`krylov_basis`, which compute + Krylov iterates and Krylov bases for several vectors at a time + EXAMPLES:: sage: A = matrix(QQ, [[5,4,2,1],[0,1,-1,-1],[-1,-1,3,0],[1,1,-1,2]]) @@ -19113,9 +19123,9 @@ cdef class Matrix(Matrix1): def _krylov_row_coordinates(self, shifts, degrees, row_pairs=None): r""" - Helper function for ``krylov_matrix`` and ``krylov_basis``. Returns a - sorted set of triplets corresponding to row coordinates, for a given set of - shifts and degrees. + Helper function for :meth:`krylov_matrix` and :meth:`krylov_basis`. + Returns a sorted set of triplets corresponding to row coordinates, for + a given set of shifts and degrees. INPUT: @@ -19123,17 +19133,20 @@ cdef class Matrix(Matrix1): the priority given to rows. - ``degrees`` -- an integer vector of length ``self.nrows()`` defining the maximum degree for rows. - - ``row_pairs`` -- the list of row coordinates to be sorted. If - ``None``, the default is - ``[(0,0),...,(self.nrows()-1,0),(0,1),...,(self.nrows()-1,1),...,(0,degrees[0]),...,(self.nrows()-1,degrees[self.nrows()-1])]``. + - ``row_pairs`` -- the list of pairs of row indices and associated + degrees to be sorted. If ``None``, the default is taken as + ``[(0,0),...,(m-1,0), (0,1),...,(m-1,1), ..., + (0,degrees[0]),...,(m-1,degrees[m-1])]``, where `m` is + `self.nrows()`. OUTPUT: - A list of triplets ``(c, d, i)`` sorted in ascending order by - ``shifts[c] + d`` then by ``c``. The entry ``i`` is the index of the - triplet after filtering but before sorting. + A list of triplets ``(c, d, i)`` sorted in ascending order, using the + lexicographic order on pairs ``(shifts[c] + d, c)``. The entry ``i`` is + the index of the triplet after filtering (according to the degree + bounds and the presence in ``row_pairs``) but before sorting. - EXAMPLES: + EXAMPLES:: sage: R = GF(97) sage: E = matrix.zero(R,3) @@ -19181,33 +19194,53 @@ cdef class Matrix(Matrix1): def krylov_matrix(self, M, shifts=None, degrees=None): r""" - Return the block matrix built from the rows of ``self`` and the matrix - ``M``, with rows ordered according to a priority defined by - ``shifts``. See - [Beckermann and Labahn, 2000, https://doi.org/10.1137/S0895479897326912]_ - and - [Jeannerod, Neiger, Schost, Villard, 2017, https://doi.org/10.1016/j.jsc.2016.11.015]_." - - The following example uses `E` to refer to ``self``, and `d` to refer - to ``degrees`` (in the case where a single integer is provided). This is - the case where ``shifts`` is `[0,...,0]``; another shift will - correspond to some row permutation of this matrix. - - [ ] - [ E ] - [ ] - [-----] - [ ] - [ EM ] - [ ] - [-----] - [ . ] - [ . ] - [ . ] - [-----] - [ ] - [ EM^d] - [ ] + Return the Krylov matrix built from the rows of ``self`` and + multiplication matrix ``M``. Here, ``self`` is some `m \times n` matrix + `E`, and ``M`` is a square `n \times n` matrix; ``degrees`` is a list + of `m` nonnegative integers `[d_0,\ldots,d_{m-1}]`. This Krylov matrix + has `d_0+\cdots+d_{m-1}+m` rows and `n` columns and is formed by + stacking the Krylov iterates `E_{i,:} \cdot M**j` for all `0 \le i < m` + and `0 \le j \le d_i`. These rows are ordered according to a priority + defined by ``shifts``. + + By default, ``shifts`` is taken as `[0,\ldots,0]`. If a single integer + `d` is provided for ``degrees``, then it is interpreted as + `[d,\ldots,d]`, and by default ``degrees`` is taken as `[n, \ldots, + n]`. + + For example, for the default ``shifts`` equal to `[0,\ldots,0]` and for + ``degrees`` equal to `[d,\ldots,d]`, the returned matrix is equal to + ``matrix.block([[self * M**j] for j in range(d+1)])``, that is, + + .. MATH:: + + \begin{bmatrix} + E \\ + E M \\ + \vdots \\ + E M^d + \end{bmatrix} . + + Another classical example is when ``shifts`` is very unbalanced, + such as `[0,n,\ldots,(m-1)n]`: then, for ``degrees`` being + `[d_0,\ldots,d_{m-1}]`, the Krylov matrix is + + .. MATH:: + + \begin{bmatrix} + E_{0,*} \\ + E_{0,*} M \\ + \vdots \\ + E_{0,*} M^{d_0} \\ + \vdots + E_{m-1,*} \\ + E_{m-1,*} M \\ + \vdots \\ + E_{m-1,*} M^{d_{m-1}} + \end{bmatrix} . + + Other shifts will correspond to some row permutation of this matrix. + (see [BL2000]_ and [JNSV2017]_). INPUT: @@ -19215,14 +19248,21 @@ cdef class Matrix(Matrix1): ``self``. - ``shifts`` -- list of ``self.nrows()`` integers (optional), row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- the maximum power or list of ``self.nrows()`` maximum - powers of ``M`` for each row in ``self`` in the output. If None, - ``self.ncols()`` is chosen by default for all rows. + - ``degrees`` -- the list of ``self.nrows()`` maximum powers of ``M`` + for each row in ``self`` in the output. If None, ``self.ncols()`` is + chosen by default for all rows. Giving a single integer as input is + equivalent to giving a list with this integer repeated + ``self.nrows()`` times. OUTPUT: - - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by - ``shifts``. + - The Krylov matrix of ``self`` and ``M``, with degree bounds + ``degrees`` and rows ordered according to ``shifts``. + + .. SEEALSO:: + + :meth:`krylov_basis`, + :meth:`krylov_kernel_basis` EXAMPLES:: @@ -19344,34 +19384,8 @@ cdef class Matrix(Matrix1): def _krylov_basis_naive(self, M, shifts, degrees, output_rows): r""" - Refer to `krylov_basis`. - - .. WARNING:: - - If the degree bounds are not true upper bounds, no guarantees are - made on the value of the output. - - INPUT: - - - ``M`` -- square matrix used in the Krylov construction. - - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority - row shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds - on the power of ``M`` for each row in ``self`` in the - ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper - bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether the indices of the rows - returned are also provided. - - OUTPUT: - - - krylov_basis: the submatrix of the krylov matrix of ``self`` and - ``M`` formed by its lexicographically-first independent rows. - - row_profile (returned if ``output_rows`` is ``True``): list of the - ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov - basis where ``i`` is the row index of the row in the krylov matrix, - ``c`` is the corresponding row in ``self`` and ``d`` is the - corresponding power of ``M``. + See :meth:`krylov_basis` for the description of this method. However, + unlike :meth:`krylov_basis`, this method performs no input validation. EXAMPLES:: @@ -19513,12 +19527,8 @@ cdef class Matrix(Matrix1): def _krylov_basis_elimination(self, M, shifts, degrees, output_rows): r""" - Refer to `krylov_basis`. - - .. WARNING:: - - If the degree bounds are not true upper bounds, no guarantees are - made on the value of the output. + See :meth:`krylov_basis` for the description of this method. However, + unlike :meth:`krylov_basis`, this method performs no input validation. EXAMPLES:: @@ -19777,6 +19787,12 @@ cdef class Matrix(Matrix1): ``c`` is the corresponding row in ``self`` and ``d`` is the corresponding power of ``M``. + .. SEEALSO:: + + :meth:`cyclic_subspace`, + :meth:`krylov_matrix`, + :meth:`krylov_kernel_basis` + EXAMPLES:: sage: R = GF(97) @@ -19990,6 +20006,12 @@ cdef class Matrix(Matrix1): - the rows of ``C`` form a minimal basis for the kernel of ``self.striped_krylov_matrix(M,P.degree())`` + .. SEEALSO:: + + :meth:`cyclic_subspace`, + :meth:`krylov_matrix`, + :meth:`krylov_basis` + EXAMPLES:: sage: R = GF(97) From dddfd5ede440dab92d4a5ad908d99dced28bda20 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 18:32:28 +0200 Subject: [PATCH 34/57] documentation improvements --- conftest.py | 343 ---------------------- src/doc/en/reference/references/index.rst | 8 +- src/sage/matrix/matrix2.pyx | 75 +++-- 3 files changed, 41 insertions(+), 385 deletions(-) delete mode 100644 conftest.py diff --git a/conftest.py b/conftest.py deleted file mode 100644 index 98fc948afa7..00000000000 --- a/conftest.py +++ /dev/null @@ -1,343 +0,0 @@ -# pyright: strict -"""Configuration and fixtures for pytest. - -This file configures pytest and provides some global fixtures. -See https://docs.pytest.org/en/latest/index.html for more details. -""" - -from __future__ import annotations - -import doctest -import inspect -import sys -import warnings -from pathlib import Path -from typing import Any, Iterable, Optional - -import pytest -from _pytest.doctest import ( - DoctestItem, - DoctestModule, - _get_continue_on_failure, - _get_runner, - _is_mocked, - _patch_unwrap_mock_aware, - get_optionflags, -) -from _pytest.pathlib import ImportMode, import_path - -from sage.doctest.forker import ( - init_sage, - showwarning_with_traceback, -) -from sage.doctest.parsing import SageDocTestParser, SageOutputChecker - - -class SageDoctestModule(DoctestModule): - """ - This is essentially a copy of `DoctestModule` from - https://github.com/pytest-dev/pytest/blob/main/src/_pytest/doctest.py. - The only change is that we use `SageDocTestParser` to extract the doctests - and `SageOutputChecker` to verify the output. - """ - - def collect(self) -> Iterable[DoctestItem]: - import doctest - - class MockAwareDocTestFinder(doctest.DocTestFinder): - """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. - https://github.com/pytest-dev/pytest/issues/3456 - https://bugs.python.org/issue25532 - """ - - def __init__(self) -> None: - super().__init__(parser=SageDocTestParser(set(["sage"]))) - - def _find_lineno(self, obj, source_lines): - """Doctest code does not take into account `@property`, this - is a hackish way to fix it. https://bugs.python.org/issue17446 - Wrapped Doctests will need to be unwrapped so the correct - line number is returned. This will be reported upstream. #8796 - """ - if isinstance(obj, property): - obj = getattr(obj, "fget", obj) - - if hasattr(obj, "__wrapped__"): - # Get the main obj in case of it being wrapped - obj = inspect.unwrap(obj) - - # Type ignored because this is a private function. - return super()._find_lineno( # type:ignore[misc] - obj, - source_lines, - ) - - def _find( - self, tests, obj, name, module, source_lines, globs, seen - ) -> None: - if _is_mocked(obj): - return - with _patch_unwrap_mock_aware(): - # Type ignored because this is a private function. - super()._find( # type:ignore[misc] - tests, obj, name, module, source_lines, globs, seen - ) - - if self.path.name == "conftest.py": - module = self.config.pluginmanager._importconftest( - self.path, - self.config.getoption("importmode"), - rootpath=self.config.rootpath, - consider_namespace_packages=True, - ) - else: - try: - module = import_path( - self.path, - mode=ImportMode.importlib, - root=self.config.rootpath, - consider_namespace_packages=True, - ) - except ImportError as exception: - if self.config.getvalue("doctest_ignore_import_errors"): - pytest.skip("unable to import module %r" % self.path) - else: - if isinstance(exception, ModuleNotFoundError): - # Ignore some missing features/modules for now - # TODO: Remove this once all optional things are using Features - if exception.name in ( - "valgrind", - "rpy2", - "sage.libs.coxeter3.coxeter", - ): - pytest.skip( - f"unable to import module { self.path } due to missing feature { exception.name }" - ) - raise - # Uses internal doctest module parsing mechanism. - finder = MockAwareDocTestFinder() - optionflags = get_optionflags(self.config) - from sage.features import FeatureNotPresentError - - runner = _get_runner( - verbose=False, - optionflags=optionflags, - checker=SageOutputChecker(), - continue_on_failure=_get_continue_on_failure(self.config), - ) - try: - for test in finder.find(module, module.__name__): - if test.examples: # skip empty doctests - yield DoctestItem.from_parent( - self, name=test.name, runner=runner, dtest=test - ) - except FeatureNotPresentError as exception: - pytest.skip( - f"unable to import module { self.path } due to missing feature { exception.feature.name }" - ) - except ModuleNotFoundError as exception: - # TODO: Remove this once all optional things are using Features - pytest.skip( - f"unable to import module { self.path } due to missing module { exception.name }" - ) - - -class IgnoreCollector(pytest.Collector): - """ - Ignore a file. - """ - - def __init__(self, parent: pytest.Collector) -> None: - super().__init__("ignore", parent) - - def collect(self) -> Iterable[pytest.Item | pytest.Collector]: - return [] - - -def pytest_collect_file( - file_path: Path, parent: pytest.Collector -) -> pytest.Collector | None: - """ - This hook is called when collecting test files, and can be used to - modify the file or test selection logic by returning a list of - ``pytest.Item`` objects which the ``pytest`` command will directly - add to the list of test items. - - See `pytest documentation `_. - """ - if ( - file_path.parent.name == "combinat" - or file_path.parent.parent.name == "combinat" - ): - # Crashes CI for some reason - return IgnoreCollector.from_parent(parent) - if file_path.suffix == ".pyx": - # We don't allow pytests to be defined in Cython files. - # Normally, Cython files are filtered out already by pytest and we only - # hit this here if someone explicitly runs `pytest some_file.pyx`. - return IgnoreCollector.from_parent(parent) - elif file_path.suffix == ".py": - if parent.config.option.doctest: - if file_path.name == "__main__.py" or file_path.name == "setup.py": - # We don't allow tests to be defined in __main__.py/setup.py files (because their import will fail). - return IgnoreCollector.from_parent(parent) - if ( - ( - file_path.name == "postprocess.py" - and file_path.parent.name == "nbconvert" - ) - or ( - file_path.name == "giacpy-mkkeywords.py" - and file_path.parent.name == "autogen" - ) - or ( - file_path.name == "flint_autogen.py" - and file_path.parent.name == "autogen" - ) - ): - # This is an executable file. - return IgnoreCollector.from_parent(parent) - - if ( - ( - file_path.name == "finite_dimensional_lie_algebras_with_basis.py" - and file_path.parent.name == "categories" - ) - or ( - file_path.name == "__init__.py" - and file_path.parent.name == "crypto" - ) - or (file_path.name == "__init__.py" and file_path.parent.name == "mq") - ): - # TODO: Fix these (import fails with "RuntimeError: dictionary changed size during iteration") - return IgnoreCollector.from_parent(parent) - - if ( - file_path.name in ("forker.py", "reporting.py") - ) and file_path.parent.name == "doctest": - # Fails with many errors due to different testing framework - return IgnoreCollector.from_parent(parent) - - if ( - ( - file_path.name == "arithgroup_generic.py" - and file_path.parent.name == "arithgroup" - ) - or ( - file_path.name == "pari.py" - and file_path.parent.name == "lfunctions" - ) - or ( - file_path.name == "permgroup_named.py" - and file_path.parent.name == "perm_gps" - ) - or ( - file_path.name == "finitely_generated.py" - and file_path.parent.name == "matrix_gps" - ) - or ( - file_path.name == "libgap_mixin.py" - and file_path.parent.name == "groups" - ) - or ( - file_path.name == "finitely_presented.py" - and file_path.parent.name == "groups" - ) - or ( - file_path.name == "classical_geometries.py" - and file_path.parent.name == "generators" - ) - ): - # Fails with "Fatal Python error" - return IgnoreCollector.from_parent(parent) - - return SageDoctestModule.from_parent(parent, path=file_path) - - -def pytest_addoption(parser): - # Add a command line option to run doctests - # (we don't use the built-in --doctest-modules option because then doctests are collected twice) - group = parser.getgroup("collect") - group.addoption( - "--doctest", - action="store_true", - default=False, - help="Run doctests in all .py modules", - dest="doctest", - ) - - -# Monkey patch exception printing to replace the full qualified name of the exception by its short name -# TODO: Remove this hack once migration to pytest is complete -import traceback - -old_format_exception_only = traceback.format_exception_only - - -def format_exception_only(etype: type, value: BaseException) -> list[str]: - formatted_exception = old_format_exception_only(etype, value) - exception_name = etype.__name__ - if etype.__module__: - exception_full_name = etype.__module__ + "." + etype.__qualname__ - else: - exception_full_name = etype.__qualname__ - - for i, line in enumerate(formatted_exception): - if line.startswith(exception_full_name): - formatted_exception[i] = line.replace( - exception_full_name, exception_name, 1 - ) - return formatted_exception - - -# Initialize Sage-specific doctest stuff -init_sage() - -# Monkey patch doctest to use our custom printer etc -old_run = doctest.DocTestRunner.run - - -def doctest_run( - self: doctest.DocTestRunner, - test: doctest.DocTest, - compileflags: Optional[int] = None, - out: Any = None, - clear_globs: bool = True, -) -> doctest.TestResults: - from sage.repl.rich_output import get_display_manager - from sage.repl.user_globals import set_globals - - traceback.format_exception_only = format_exception_only - - # Display warnings in doctests - warnings.showwarning = showwarning_with_traceback - setattr(sys, "__displayhook__", get_display_manager().displayhook) - - # Ensure that injecting globals works as expected in doctests - set_globals(test.globs) - return old_run(self, test, compileflags, out, clear_globs) - - -doctest.DocTestRunner.run = doctest_run - - -@pytest.fixture(autouse=True, scope="session") -def add_imports(doctest_namespace: dict[str, Any]): - """ - Add global imports for doctests. - - See `pytest documentation `. - """ - # Inject sage.all into each doctest - import sage.repl.ipython_kernel.all_jupyter - - dict_all = sage.repl.ipython_kernel.all_jupyter.__dict__ - - # Remove '__package__' item from the globals since it is not - # always in the globals in an actual Sage session. - dict_all.pop("__package__", None) - - sage_namespace = dict(dict_all) - sage_namespace["__name__"] = "__main__" - - doctest_namespace.update(**sage_namespace) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index beca855348d..23c42c0afc1 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -1011,10 +1011,10 @@ REFERENCES: Anal. Appl. 15 (1994) 804-823. :doi:`10.1137/S0895479892230031` -.. [BL2000] Bernhard Beckermann, George Labahn. *Fraction-Free Computation of - Matrix Rational Interpolants and Matrix GCDs*. SIAM J. Matrix - Anal. Appl. 22 (2000) 114-144. - :doi:`10.1137/S0895479897326912` +.. [BecLab2000] Bernhard Beckermann, George Labahn. *Fraction-Free Computation + of Matrix Rational Interpolants and Matrix GCDs*. SIAM J. + Matrix Anal. Appl. 22 (2000) 114-144. + :doi:`10.1137/S0895479897326912` .. [BL1995] \W. Bosma, H.W. Lenstra: *Complete Systems of Two Addition Laws for Elliptic Curves*. Journal of Number Theory, volume 53, issue 2, diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 2750b20cda2..174ea0f56f0 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -13206,7 +13206,7 @@ cdef class Matrix(Matrix1): .. SEEALSO:: :meth:`krylov_matrix` and :meth:`krylov_basis`, which compute - Krylov iterates and Krylov bases for several vectors at a time + Krylov iterates and Krylov bases for several vectors at a time. ALGORITHM: @@ -13340,7 +13340,7 @@ cdef class Matrix(Matrix1): .. SEEALSO:: :meth:`krylov_matrix` and :meth:`krylov_basis`, which compute - Krylov iterates and Krylov bases for several vectors at a time + Krylov iterates and Krylov bases for several vectors at a time. EXAMPLES:: @@ -19135,9 +19135,8 @@ cdef class Matrix(Matrix1): the maximum degree for rows. - ``row_pairs`` -- the list of pairs of row indices and associated degrees to be sorted. If ``None``, the default is taken as - ``[(0,0),...,(m-1,0), (0,1),...,(m-1,1), ..., - (0,degrees[0]),...,(m-1,degrees[m-1])]``, where `m` is - `self.nrows()`. + `[(i,j), 0 \le i < m, 0 \le j \le d_i]` where `m` is + ``self.nrows()`` and `d_i` is ``degrees[i]``. OUTPUT: @@ -19194,23 +19193,22 @@ cdef class Matrix(Matrix1): def krylov_matrix(self, M, shifts=None, degrees=None): r""" - Return the Krylov matrix built from the rows of ``self`` and - multiplication matrix ``M``. Here, ``self`` is some `m \times n` matrix - `E`, and ``M`` is a square `n \times n` matrix; ``degrees`` is a list - of `m` nonnegative integers `[d_0,\ldots,d_{m-1}]`. This Krylov matrix - has `d_0+\cdots+d_{m-1}+m` rows and `n` columns and is formed by - stacking the Krylov iterates `E_{i,:} \cdot M**j` for all `0 \le i < m` - and `0 \le j \le d_i`. These rows are ordered according to a priority - defined by ``shifts``. - - By default, ``shifts`` is taken as `[0,\ldots,0]`. If a single integer - `d` is provided for ``degrees``, then it is interpreted as - `[d,\ldots,d]`, and by default ``degrees`` is taken as `[n, \ldots, - n]`. - - For example, for the default ``shifts`` equal to `[0,\ldots,0]` and for - ``degrees`` equal to `[d,\ldots,d]`, the returned matrix is equal to - ``matrix.block([[self * M**j] for j in range(d+1)])``, that is, + Return the Krylov matrix built from the rows of ``self`` and using + multiplication matrix `M`. Here, ``self`` is some `m \times n` matrix + `E`, and `M` is a square `n \times n` matrix; ``degrees`` is a list of + `m` nonnegative integers `[d_0,\ldots,d_{m-1}]`. This Krylov matrix has + `d_0+\cdots+d_{m-1}+m` rows and `n` columns and is formed by stacking + the Krylov iterates `E_{i,:} \cdot M^j` for all `0 \le i < m` and `0 + \le j \le d_i`. These rows are ordered according to a priority defined + by ``shifts``. + + By default, ``shifts`` is taken as `[0,\ldots,0]`, and ``degrees`` is + taken as `[n, \ldots, n]`. If a single integer `d` is provided for + ``degrees``, then it is interpreted as `[d,\ldots,d]`. + + For example, for the default ``shifts`` equal to `[0, \ldots, 0]` and + for ``degrees`` equal to `[d,\ldots,d]`, the returned matrix is equal + to .. MATH:: @@ -19221,9 +19219,8 @@ cdef class Matrix(Matrix1): E M^d \end{bmatrix} . - Another classical example is when ``shifts`` is very unbalanced, - such as `[0,n,\ldots,(m-1)n]`: then, for ``degrees`` being - `[d_0,\ldots,d_{m-1}]`, the Krylov matrix is + Another classical case is when ``shifts`` is + `[d_0,d_0+d_1,\ldots,d_0+\cdots+d_{m-1}]`: then the Krylov matrix is .. MATH:: @@ -19232,37 +19229,39 @@ cdef class Matrix(Matrix1): E_{0,*} M \\ \vdots \\ E_{0,*} M^{d_0} \\ - \vdots + \vdots \\ E_{m-1,*} \\ E_{m-1,*} M \\ \vdots \\ E_{m-1,*} M^{d_{m-1}} \end{bmatrix} . - Other shifts will correspond to some row permutation of this matrix. - (see [BL2000]_ and [JNSV2017]_). + Other shifts will give rise to some row permutation of the latter + matrix (see [BL2000]_ and [JNSV2017]_). INPUT: - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - ``shifts`` -- list of ``self.nrows()`` integers (optional), row - priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- the list of ``self.nrows()`` maximum powers of ``M`` - for each row in ``self`` in the output. If None, ``self.ncols()`` is - chosen by default for all rows. Giving a single integer as input is - equivalent to giving a list with this integer repeated - ``self.nrows()`` times. + priority shifts. Defaults to all zeroes. + - ``degrees`` -- list of ``self.nrows()`` integers (optional), the + `i`-th entry ``degrees[i]`` indicating the number of Krylov iterates + to appear in the output (that is, ``self[i,:] * M**j`` will appear + for `j` up to ``degrees[i]``, included). Defaults to ``self.ncols()`` + for all rows. Giving a single integer for ``degrees`` is equivalent + to giving a list with this integer repeated ``self.nrows()`` times. OUTPUT: - - The Krylov matrix of ``self`` and ``M``, with degree bounds - ``degrees`` and rows ordered according to ``shifts``. + - The Krylov matrix of ``self`` and ``M``, with respect to degree + bounds ``degrees`` and with rows ordered according to ``shifts``. .. SEEALSO:: - :meth:`krylov_basis`, - :meth:`krylov_kernel_basis` + :meth:`krylov_basis` computes a basis of the row space of the + Krylov matrix, whereas :meth:`krylov_kernel_basis` computes a + compact representation of its left kernel. EXAMPLES:: From 9c6e7b8bc72739e84e65f2d3982f91347de38676 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 20:03:13 +0200 Subject: [PATCH 35/57] update doc and tests/examples --- src/sage/matrix/matrix2.pyx | 239 +++++------------------------------- 1 file changed, 34 insertions(+), 205 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 7cd6b841480..41b4c40ee05 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19387,117 +19387,28 @@ cdef class Matrix(Matrix1): See :meth:`krylov_basis` for the description of this method. However, unlike :meth:`krylov_basis`, this method performs no input validation. - EXAMPLES:: + TESTS:: sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: M - [0 1 0] - [0 0 1] - [0 0 0] - sage: degrees = 3 - sage: K = E.krylov_matrix(M) - sage: K - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,algorithm='naive') + sage: E = matrix(R, [[27,49,29],[50,58,0],[77,10,29]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E._krylov_basis_naive(M, [0, 0, 0], [3, 3, 3], True) ( [27 49 29] [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] - sage: rows - [(0, 0, 0), - (1, 0, 1), - (2, 0, 2), - (0, 1, 3), - (1, 1, 4), - (2, 1, 5), - (0, 2, 6), - (1, 2, 7), - (2, 2, 8), - (0, 3, 9), - (1, 3, 10), - (2, 3, 11)] + sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(0, 0, 0), - (0, 1, 3), - (0, 2, 6), - (0, 3, 9), - (1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (1, 3, 10), - (2, 0, 2), - (2, 1, 5), - (2, 2, 8), - (2, 3, 11)] - sage: E.krylov_matrix(M,shifts=shifts) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='naive') + sage: E._krylov_basis_naive(M, shifts, [3, 3, 3], True) ( [27 49 29] [ 0 27 49] [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) + sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (2, 0, 2), - (0, 0, 0), - (1, 3, 10), - (2, 1, 5), - (0, 1, 3), - (2, 2, 8), - (0, 2, 6), - (2, 3, 11), - (0, 3, 9)] - sage: E.krylov_matrix(M,shifts=shifts) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='naive') + sage: E._krylov_basis_naive(M, shifts, [3, 3, 3], True) ( [50 58 0] [ 0 50 58] @@ -19514,16 +19425,16 @@ cdef class Matrix(Matrix1): row_profile = K.pivot_rows() # construct submatrix - pivot = K.matrix_from_rows(row_profile) + kmat = K.matrix_from_rows(row_profile) if not output_rows: - return pivot + return kmat row_coordinates = self._krylov_row_coordinates(shifts, degrees) row_profile = tuple([(*row_coordinates[i][:2], i) for i in row_profile]) - return pivot, row_profile + return kmat, row_profile def _krylov_basis_elimination(self, M, shifts, degrees, output_rows): r""" @@ -19532,115 +19443,28 @@ cdef class Matrix(Matrix1): EXAMPLES:: + TESTS:: + sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: M - [0 1 0] - [0 0 1] - [0 0 0] - sage: degrees = 3 - sage: K = E.krylov_matrix(M) - sage: K - [27 49 29] - [50 58 0] - [77 10 29] - [ 0 27 49] - [ 0 50 58] - [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,output_rows=True,algorithm='elimination') + sage: E = matrix(R, [[27,49,29],[50,58,0],[77,10,29]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E._krylov_basis_elimination(M, [0, 0, 0], [3, 3, 3], True) ( [27 49 29] [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] - sage: rows - [(0, 0, 0), - (1, 0, 1), - (2, 0, 2), - (0, 1, 3), - (1, 1, 4), - (2, 1, 5), - (0, 2, 6), - (1, 2, 7), - (2, 2, 8), - (0, 3, 9), - (1, 3, 10), - (2, 3, 11)] + sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(0, 0, 0), - (0, 1, 3), - (0, 2, 6), - (0, 3, 9), - (1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (1, 3, 10), - (2, 0, 2), - (2, 1, 5), - (2, 2, 8), - (2, 3, 11)] - sage: E.krylov_matrix(M,shifts=shifts) - [27 49 29] - [ 0 27 49] - [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + sage: E._krylov_basis_elimination(M, shifts, [3, 3, 3], True) ( [27 49 29] [ 0 27 49] [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) + sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (2, 0, 2), - (0, 0, 0), - (1, 3, 10), - (2, 1, 5), - (0, 1, 3), - (2, 2, 8), - (0, 2, 6), - (2, 3, 11), - (0, 3, 9)] - sage: E.krylov_matrix(M,shifts=shifts) - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [77 10 29] - [27 49 29] - [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + sage: E._krylov_basis_elimination(M, shifts, [3, 3, 3], True) ( [50 58 0] [ 0 50 58] @@ -19789,9 +19613,11 @@ cdef class Matrix(Matrix1): .. SEEALSO:: - :meth:`cyclic_subspace`, - :meth:`krylov_matrix`, - :meth:`krylov_kernel_basis` + :meth:`cyclic_subspace` provides similar functionality for a single + vector; :meth:`krylov_matrix` computes a Krylov matrix (without + discarding linearly redundant rows); :meth:`krylov_kernel_basis` + computes a compact representation of the left kernel of a + Krylov matrix. EXAMPLES:: @@ -19806,7 +19632,6 @@ cdef class Matrix(Matrix1): [0 1 0] [0 0 1] [0 0 0] - sage: degrees = 3 sage: K = E.krylov_matrix(M) sage: K [27 49 29] @@ -19827,6 +19652,10 @@ cdef class Matrix(Matrix1): [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) + + Row ordering for the uniform zero shift:: + + sage: degrees = 3 # default in above constructions sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] sage: rows [(0, 0, 0), @@ -19940,7 +19769,7 @@ cdef class Matrix(Matrix1): shifts = (ZZ**E.nrows())(shifts) else: raise ValueError(f"krylov_basis: shifts is not an integer vector of length {E.nrows()}.") - + if algorithm is None: if E.base_ring().order() == 2: if E.nrows() <= 12: @@ -20008,9 +19837,9 @@ cdef class Matrix(Matrix1): .. SEEALSO:: - :meth:`cyclic_subspace`, - :meth:`krylov_matrix`, - :meth:`krylov_basis` + :meth:`krylov_basis` computes a basis of the row space of the + Krylov matrix; :meth:`krylov_matrix` computes the full Krylov + matrix (without discarding linearly redundant rows). EXAMPLES:: @@ -20115,7 +19944,7 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): raise NotImplementedError("krylov_kernel_basis: matrix entries must come from an exact field") - + if not isinstance(M, Matrix): raise TypeError("krylov_kernel_basis: M is not a matrix") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): From 0ccb4bb033be28bca749effca32f98b65d95cb82 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 21:39:23 +0200 Subject: [PATCH 36/57] document and examples --- src/sage/matrix/matrix2.pyx | 342 ++++++++++++++++++------------------ 1 file changed, 175 insertions(+), 167 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 41b4c40ee05..5a8ecad68a7 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19220,8 +19220,9 @@ cdef class Matrix(Matrix1): E M^d \end{bmatrix} . - Another classical case is when ``shifts`` is - `[d_0,d_0+d_1,\ldots,d_0+\cdots+d_{m-1}]`: then the Krylov matrix is + Another classical case is when ``shifts`` is `[0,k,2k,\ldots,(m-1)k]` + for a large `k` such as `k = d_0+\cdots+d_{m-1}`: then the Krylov + matrix is .. MATH:: @@ -19237,21 +19238,22 @@ cdef class Matrix(Matrix1): E_{m-1,*} M^{d_{m-1}} \end{bmatrix} . - Other shifts will give rise to some row permutation of the latter - matrix (see [BL2000]_ and [JNSV2017]_). + Other shifts will yield some row permutation of the latter matrix (see + [BL2000]_ and [JNSV2017]_). INPUT: - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - ``shifts`` -- list of ``self.nrows()`` integers (optional), row - priority shifts. Defaults to all zeroes. - - ``degrees`` -- list of ``self.nrows()`` integers (optional), the - `i`-th entry ``degrees[i]`` indicating the number of Krylov iterates - to appear in the output (that is, ``self[i,:] * M**j`` will appear - for `j` up to ``degrees[i]``, included). Defaults to ``self.ncols()`` - for all rows. Giving a single integer for ``degrees`` is equivalent - to giving a list with this integer repeated ``self.nrows()`` times. + priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + (optional). The `i`-th entry ``degrees[i]`` indicates the number of + Krylov iterates to appear in the output (that is, ``self[i,:] * + M**j`` will appear for `j` up to ``degrees[i]``, included). If + ``None``, defaults to ``self.ncols()`` for all rows. Giving a single + integer for ``degrees`` is equivalent to giving a list with this + integer repeated ``self.nrows()`` times. OUTPUT: @@ -19261,18 +19263,18 @@ cdef class Matrix(Matrix1): .. SEEALSO:: :meth:`krylov_basis` computes a basis of the row space of the - Krylov matrix, whereas :meth:`krylov_kernel_basis` computes a + Krylov matrix; whereas :meth:`krylov_kernel_basis` computes a compact representation of its left kernel. EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) sage: E [27 49 29] [50 58 0] [77 10 29] - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) sage: M [0 1 0] [0 0 1] @@ -19290,7 +19292,45 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] [ 0 0 0] - sage: E.krylov_matrix(M,[0,3,6]) + + Row ordering for the zero shift:: + + sage: degrees = 3 # default in above constructions + sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) + for i in range(E.nrows())] + sage: rows + [(0, 0, 0), + (1, 0, 1), + (2, 0, 2), + (0, 1, 3), + (1, 1, 4), + (2, 1, 5), + (0, 2, 6), + (1, 2, 7), + (2, 2, 8), + (0, 3, 9), + (1, 3, 10), + (2, 3, 11)] + + Trying the other above-mentioned classical shift:: + + sage: shifts = [0,3,6] + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) + sage: rows + [(0, 0, 0), + (0, 1, 3), + (0, 2, 6), + (0, 3, 9), + (1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (1, 3, 10), + (2, 0, 2), + (2, 1, 5), + (2, 2, 8), + (2, 3, 11)] + + sage: E.krylov_matrix(M, shifts) [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19303,7 +19343,27 @@ cdef class Matrix(Matrix1): [ 0 77 10] [ 0 0 77] [ 0 0 0] - sage: E.krylov_matrix(M,[3,0,2]) + + + With another shift:: + + sage: shifts = [3,0,2] + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) + sage: rows + [(1, 0, 1), + (1, 1, 4), + (1, 2, 7), + (2, 0, 2), + (0, 0, 0), + (1, 3, 10), + (2, 1, 5), + (0, 1, 3), + (2, 2, 8), + (0, 2, 6), + (2, 3, 11), + (0, 3, 9)] + + sage: E.krylov_matrix(M, shifts) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19316,7 +19376,10 @@ cdef class Matrix(Matrix1): [ 0 0 27] [ 0 0 0] [ 0 0 0] - sage: E.krylov_matrix(M,[3,0,2],[0,2,1]) + + Specifying degree bounds via ``degrees``:: + + sage: E.krylov_matrix(M, shifts, degrees=[0,2,1]) [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19575,169 +19638,160 @@ cdef class Matrix(Matrix1): def krylov_basis(self, M, shifts=None, degrees=None, output_rows=True, algorithm=None): r""" - Compute the rank profile (row and column) of the block Krylov matrix - built from ``self`` and matrix ``M``. + Return the matrix `B` formed by stacking the first `r` linearly + independent row vectors `E_{i,:} \cdot M^j`, for `0 \le i < m` and `j + \ge 0` when they are ordered according to ``shifts``. Here `E` is the + input `m \times n` matrix, `M` is a square `n \times n` matrix, and `r` + is dimension of Krylov subspace of ``self`` and `M`, that is, the span + of all the above row vectors. + + A list ``degrees`` of integers `[d_0,\ldots,d_{m-1}]` can be provided + to indicate known maximal exponents: `E_{k,*} \cdot M^{d_k-1}` may + appear in the output basis, but `E_{k,*} \cdot M^{d_k-1}` will not. In + other words, `E_{k,*} \cdot M^{d_k}` is known to linearly depend on the + set of row vectors `E_{i,*} \cdot M^{j}` that appear before it + according to the order defined by ``shifts``. It is always valid to + take ``degrees`` as `[d, \ldots, d]` where `d` is the degree of the + minimal polynomial of `M`. By default, the implementation takes + ``degrees`` as `[n, \ldots, n]`. + + This method incidentally computes the column rank profile (as returned + by :meth:`pivots`) of its output matrix, and so, it is cached in the + output for faster future access. + + By default, the method also returns information that relate the + computed basis rows to the corresponding rows in the Krylov matrix `K` + as if built with :meth:`krylov_matrix` with the same parameters. + Specifically, for each row of the output, this gives its position in + `K` (thus forming the row rank profile of `K`), as well as pairs `i, j` + indicating that the row is `E_{i,*} \cdot M^{j}`. .. WARNING:: - If the degree bounds are not true upper bounds, no guarantees are - made on the value of the output, including whether it is consistent - between different algorithms. An upper bound `\delta_i` on the row - `i` is such that for every `j > \delta_i`, `E_{i,*}M^j` is - dependent on the rows before it in an "infinite" Krylov matrix. - `self.ncols()` is known to always be an upper bound. + In the case where degrees are provided and do not satisfy the above + requirements, no guarantees are provided on the value of the output, + including whether it is consistent between the different algorithms. INPUT: - - ``M`` -- square matrix used in the Krylov construction. - - ``shifts`` -- list of ``self.nrows()`` integers (optional): priority - row shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an upper bound or list of ``self.nrows()`` upper bounds - on the power of ``M`` for each row in ``self`` in the - ``krylov_basis`` of ``M`` in Krylov matrix. If None, a suitable upper - bound of ``self.ncols()`` is default for all rows. - - ``output_rows`` -- determines whether the indices of the rows - returned are also provided. + - ``M`` -- a square matrix of size equal to the number of columns of + ``self``. + - ``shifts`` -- list of ``self.nrows()`` integers (optional): row + priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + (optional). The `i`-th entry ``degrees[i]`` indicates that + ``self[i,:] * M**degrees[j]`` is known to be dependent on rows before + it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to + ``self.ncols()`` for all rows. Giving a single integer for + ``degrees`` is equivalent to giving a list with this integer repeated + ``self.nrows()`` times. + - ``output_rows`` -- determines whether information relating the output + rows to their position in the Krylov matrix is also provided. - ``algorithm`` -- either 'naive', 'elimination', or None (let Sage choose). OUTPUT: - - ``krylov_basis``: the submatrix of the krylov matrix of ``self`` and - ``M`` formed by its first independent rows. - - ``row_profile`` (returned if ``output_rows`` is ``True``): list of the - ``r`` triplets ``(c,d,i)`` corresponding to the rows of the krylov - basis where ``i`` is the row index of the row in the krylov matrix, - ``c`` is the corresponding row in ``self`` and ``d`` is the + - `B`: submatrix formed by the first `r` independent rows of the Krylov + matrix of ``self`` and `M`, with `r` as above + - ``row_profile`` (returned if ``output_rows`` is ``True``): list of + the ``r`` triplets ``(i, j, k)`` corresponding to the rows of the + Krylov basis where ``k`` is the row index of the row in the krylov + matrix, ``i`` is the corresponding row in ``self`` and ``j`` is the corresponding power of ``M``. .. SEEALSO:: - :meth:`cyclic_subspace` provides similar functionality for a single - vector; :meth:`krylov_matrix` computes a Krylov matrix (without - discarding linearly redundant rows); :meth:`krylov_kernel_basis` - computes a compact representation of the left kernel of a - Krylov matrix. + :meth:`cyclic_subspace` provides similar functionality for ``self`` + consisting of a single vector; :meth:`krylov_matrix` computes a + Krylov matrix (without discarding linearly redundant rows); + :meth:`krylov_kernel_basis` computes a compact representation of + the left kernel of a Krylov matrix. EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: M - [0 1 0] - [0 0 1] - [0 0 0] + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) sage: K = E.krylov_matrix(M) - sage: K + sage: K[:,:6] [27 49 29] [50 58 0] [77 10 29] [ 0 27 49] [ 0 50 58] [ 0 77 10] - [ 0 0 27] - [ 0 0 50] - [ 0 0 77] - [ 0 0 0] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,output_rows=True,algorithm='elimination') + sage: E.krylov_basis(M, output_rows=True, algorithm='elimination') ( [27 49 29] [50 58 0] [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - Row ordering for the uniform zero shift:: + The above matrix `K` indeed has row rank profile `(0, 1, 3)`, meaning + here that the third row of `E` is dependent on its two first rows:: - sage: degrees = 3 # default in above constructions - sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) for i in range(E.nrows())] - sage: rows + sage: E.row_pivots() == (0, 1) + True + sage: K.row_pivots() == (0, 1, 3) + True + + Observe also how the output row information relates the basis to the + rows in the Krylov matrix `K`:: + + sage: degrees = 3 # default == self.ncols() + sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) + for i in range(E.nrows())] + sage: rows[:6] [(0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), - (2, 1, 5), - (0, 2, 6), - (1, 2, 7), - (2, 2, 8), - (0, 3, 9), - (1, 3, 10), - (2, 3, 11)] + (2, 1, 5)] + + Now with another shift, the first three rows of the Krylov matrix are + independent and thus form the Krylov basis:: + sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(0, 0, 0), - (0, 1, 3), - (0, 2, 6), - (0, 3, 9), - (1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (1, 3, 10), - (2, 0, 2), - (2, 1, 5), - (2, 2, 8), - (2, 3, 11)] - sage: E.krylov_matrix(M,shifts=shifts) + sage: E.krylov_matrix(M, shifts=shifts)[:3,:] [27 49 29] [ 0 27 49] [ 0 0 27] - [ 0 0 0] - [50 58 0] - [ 0 50 58] - [ 0 0 50] - [ 0 0 0] - [77 10 29] - [ 0 77 10] - [ 0 0 77] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + sage: E.krylov_basis(M, shifts=shifts, algorithm='naive') ( [27 49 29] [ 0 27 49] [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) + sage: [(i,j) for (i,j,k) in rows[:3]] + [(0, 0), + (0, 1), + (0, 2)] + + Another shift for which the first three rows of the Krylov matrix are + independent:: + sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]],x[0])) - sage: rows - [(1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (2, 0, 2), - (0, 0, 0), - (1, 3, 10), - (2, 1, 5), - (0, 1, 3), - (2, 2, 8), - (0, 2, 6), - (2, 3, 11), - (0, 3, 9)] - sage: E.krylov_matrix(M,shifts=shifts) + sage: E.krylov_matrix(M, shifts=shifts)[:6,:] [50 58 0] [ 0 50 58] [ 0 0 50] [77 10 29] [27 49 29] [ 0 0 0] - [ 0 77 10] - [ 0 27 49] - [ 0 0 77] - [ 0 0 27] - [ 0 0 0] - [ 0 0 0] - sage: E.krylov_basis(M,shifts=shifts,algorithm='elimination') + sage: E.krylov_basis(M, shifts=shifts, algorithm='elimination') ( [50 58 0] [ 0 50 58] [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) ) + sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) + sage: [(i,j) for (i,j,k) in rows[:3]] + [(1, 0), + (1, 1), + (1, 2)] """ from sage.modules.free_module_element import vector @@ -19795,52 +19849,6 @@ cdef class Matrix(Matrix1): def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): r""" - Return a shifted minimal krylov kernel basis for (``self``,``M``) in - ``s``-Popov form with respect to a set of shifts ``s``. - - Given a K[x]-module V defined by multiplication matrix ``M``, i.e. - elements are vectors of length n in K, scalars are polynomials in `K[x]`, - and scalar multiplication is defined by p v := v p(``M``), ``self`` - represents ``self.nrows()`` elements e_1, ..., e_m in V: - - A krylov kernel basis is a set of tuples of length - ``self.nrows()`` in K[x] that form a basis for solutions to the - equation ``p_1 e_1 + ... + p_n e_n = 0``. - - See Section 6.7 of [Kai1980]_ - - .. WARNING:: - - If the degree bounds are not true upper bounds, no guarantees are - made on the correctness of the output. - - INPUT: - - - ``M`` -- square matrix for Krylov construction. - - ``shifts`` -- list of self.nrows() integers (optional): controls - priority of rows. If ``None``, defaults to all zeroes. - - ``degrees`` -- upper bound on degree of minpoly(`M`). If None, a - suitable upper bound of ``self.ncols()`` is default. - - ``var`` -- variable name for the returned m x m polynomial matrix. If - None (default), returns a m x (m*(deg+1)) matrix representing the - coefficient matrices of the output. - - OUTPUT: - - If ``var`` is ``None``, a ``self.nrows() * self.nrows()`` matrix ``P``. - If ``var`` is not ``None``, the corresponding - ``self.nrows() * (self.nrows() * (P.degree()+1))`` matrix ``C`` where - ``P[i,j][d] == C[i,m*d+j]``, such that - - ``P`` is ``s``-Popov - - the rows of ``C`` form a minimal basis for the kernel of - ``self.striped_krylov_matrix(M,P.degree())`` - - .. SEEALSO:: - - :meth:`krylov_basis` computes a basis of the row space of the - Krylov matrix; :meth:`krylov_matrix` computes the full Krylov - matrix (without discarding linearly redundant rows). - EXAMPLES:: sage: R = GF(97) From 6c742dfbe6daa1cd77c8b8a1171f52464ac61458 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 22:06:43 +0200 Subject: [PATCH 37/57] a few fixes in the documentation of krylov_basis / krylov_matrix --- src/sage/matrix/matrix2.pyx | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 5a8ecad68a7..fb824e2ff58 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19227,15 +19227,15 @@ cdef class Matrix(Matrix1): .. MATH:: \begin{bmatrix} - E_{0,*} \\ - E_{0,*} M \\ + E_{0,:} \\ + E_{0,:} M \\ \vdots \\ - E_{0,*} M^{d_0} \\ + E_{0,:} M^{d_0} \\ \vdots \\ - E_{m-1,*} \\ - E_{m-1,*} M \\ + E_{m-1,:} \\ + E_{m-1,:} M \\ \vdots \\ - E_{m-1,*} M^{d_{m-1}} + E_{m-1,:} M^{d_{m-1}} \end{bmatrix} . Other shifts will yield some row permutation of the latter matrix (see @@ -19641,19 +19641,19 @@ cdef class Matrix(Matrix1): Return the matrix `B` formed by stacking the first `r` linearly independent row vectors `E_{i,:} \cdot M^j`, for `0 \le i < m` and `j \ge 0` when they are ordered according to ``shifts``. Here `E` is the - input `m \times n` matrix, `M` is a square `n \times n` matrix, and `r` - is dimension of Krylov subspace of ``self`` and `M`, that is, the span - of all the above row vectors. + input `m \times n` matrix ``self``, `M` is a square `n \times n` + matrix, and `r` is dimension of Krylov subspace of `E` and `M`, that + is, the span of all the above row vectors. A list ``degrees`` of integers `[d_0,\ldots,d_{m-1}]` can be provided - to indicate known maximal exponents: `E_{k,*} \cdot M^{d_k-1}` may - appear in the output basis, but `E_{k,*} \cdot M^{d_k-1}` will not. In - other words, `E_{k,*} \cdot M^{d_k}` is known to linearly depend on the - set of row vectors `E_{i,*} \cdot M^{j}` that appear before it - according to the order defined by ``shifts``. It is always valid to - take ``degrees`` as `[d, \ldots, d]` where `d` is the degree of the - minimal polynomial of `M`. By default, the implementation takes - ``degrees`` as `[n, \ldots, n]`. + to indicate known maximal exponents, such that `E_{k,:} \cdot + M^{d_k-1}` may appear in the output basis, but `E_{k,:} \cdot + M^{d_k}` will not. In other words, `E_{k,:} \cdot M^{d_k}` is known to + linearly depend on the set of row vectors `E_{i,:} \cdot M^{j}` that + appear before it according to the order defined by ``shifts``. It is + always valid to take ``degrees`` as `[d, \ldots, d]` where `d` is the + degree of the minimal polynomial of `M`. By default, the implementation + takes ``degrees`` as `[n, \ldots, n]`. This method incidentally computes the column rank profile (as returned by :meth:`pivots`) of its output matrix, and so, it is cached in the @@ -19662,9 +19662,9 @@ cdef class Matrix(Matrix1): By default, the method also returns information that relate the computed basis rows to the corresponding rows in the Krylov matrix `K` as if built with :meth:`krylov_matrix` with the same parameters. - Specifically, for each row of the output, this gives its position in - `K` (thus forming the row rank profile of `K`), as well as pairs `i, j` - indicating that the row is `E_{i,*} \cdot M^{j}`. + Specifically, for each row of the output, this information gives its + position in `K` (thus forming the row rank profile of `K`), as well as + pairs `i, j` indicating that the row is `E_{i,:} \cdot M^{j}`. .. WARNING:: @@ -19680,7 +19680,7 @@ cdef class Matrix(Matrix1): priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers (optional). The `i`-th entry ``degrees[i]`` indicates that - ``self[i,:] * M**degrees[j]`` is known to be dependent on rows before + ``self[i,:] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated @@ -19692,11 +19692,11 @@ cdef class Matrix(Matrix1): OUTPUT: - - `B`: submatrix formed by the first `r` independent rows of the Krylov - matrix of ``self`` and `M`, with `r` as above + - matrix formed by the first `r` independent rows of the Krylov matrix + of ``self`` and `M`, with `r` the rank of that matrix - ``row_profile`` (returned if ``output_rows`` is ``True``): list of the ``r`` triplets ``(i, j, k)`` corresponding to the rows of the - Krylov basis where ``k`` is the row index of the row in the krylov + Krylov basis where ``k`` is the row index of the row in the Krylov matrix, ``i`` is the corresponding row in ``self`` and ``j`` is the corresponding power of ``M``. From ffe4eceb230517640aace47ad65f54180f13ad88 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 22:20:24 +0200 Subject: [PATCH 38/57] fix tests --- src/sage/matrix/matrix2.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index fb824e2ff58..817c0563108 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19297,7 +19297,7 @@ cdef class Matrix(Matrix1): sage: degrees = 3 # default in above constructions sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) - for i in range(E.nrows())] + ....: for i in range(E.nrows())] sage: rows [(0, 0, 0), (1, 0, 1), @@ -19714,7 +19714,7 @@ cdef class Matrix(Matrix1): sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) sage: K = E.krylov_matrix(M) - sage: K[:,:6] + sage: K[:6,:] [27 49 29] [50 58 0] [77 10 29] @@ -19731,9 +19731,9 @@ cdef class Matrix(Matrix1): The above matrix `K` indeed has row rank profile `(0, 1, 3)`, meaning here that the third row of `E` is dependent on its two first rows:: - sage: E.row_pivots() == (0, 1) + sage: E.pivot_rows() == (0, 1) True - sage: K.row_pivots() == (0, 1, 3) + sage: K.pivot_rows() == (0, 1, 3) True Observe also how the output row information relates the basis to the @@ -19741,7 +19741,7 @@ cdef class Matrix(Matrix1): sage: degrees = 3 # default == self.ncols() sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) - for i in range(E.nrows())] + ....: for i in range(E.nrows())] sage: rows[:6] [(0, 0, 0), (1, 0, 1), From b72a1a7ce5a35e7b76e0e2cdfbcb3a937d165e91 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Wed, 27 Aug 2025 23:36:40 +0200 Subject: [PATCH 39/57] documentation for krylov_kernel_basis --- src/sage/matrix/matrix2.pyx | 124 +++++++++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 817c0563108..24d5872bbd8 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19239,7 +19239,7 @@ cdef class Matrix(Matrix1): \end{bmatrix} . Other shifts will yield some row permutation of the latter matrix (see - [BL2000]_ and [JNSV2017]_). + [BecLab2000]_ and [JNSV2017]_). INPUT: @@ -19849,29 +19849,120 @@ cdef class Matrix(Matrix1): def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): r""" + Return a Krylov kernel basis for (``self``,``M``) in canonical + form, depending on the row priority defined by ``shifts``. + + Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov + basis `B` as computed by :meth:`krylov_basis` with the same parameters + `M`, ``shifts``, and ``degrees``. Let `[\delta_0,\ldots,\delta_{m-1}]` + be the exponents of first linear dependency for each row. That is, if + the row `E_{i,:}` has not been selected for appearing in `B` then + `\delta_i = 0`, and otherwise, `delta_i` is such that `E_{i,:} \cdot + M^{\delta_i-1}` has been selected for `B` but `E_{i,:} \cdot + M^{\delta_i}` has not. (These integers are also easily deduced from the + output triplets of :meth:`krylov_basis`.) + + The returned matrix `K` is a basis, in reduced row echelon form, of the + left nullspace of the Krylov matrix built from `E` and `M` with + ``shifts`` and ``degrees`` equal to `[\delta_0,\ldots,\delta_{m-1}]`. + Recall from :meth:`krylov_matrix`` that this matrix is, up to a row + permutation indicated by ``shifts``, equal to + + .. MATH:: + + \begin{bmatrix} + B \\ + E_{0,:} M^{\delta_0} \\ + \vdots \\ + E_{m-1,:} M^{\delta_{m-1}} + \end{bmatrix}. + + Since `B` has full row rank `r`, this kernel basis `K` has `m` rows and + `m+r` columns (where `r \le n`), and has rank `m`. Thanks to the module + structure underlying Krylov matrices, from the matrix `K` one can + deduce for free (up to copying rows and shifting coefficients to the + right) a basis of the left kernel bases for Krylov matrices that would + be built from the same `E`, `M`, and ``shifts``, but with larger + degrees, that is `[d_0,\ldots,d_{m-1}]` with `d_i \ge \delta_i` for all + `i`. + + This matrix `K` can also be represented as a nonsingular univariate + polynomial matrix of size `m \times m` in ``shifts``-Popov form (see + :meth:`sage.matrix.matrix_polynomial_dense.popov_form` for + definitions). This is provided as an option via the input ``var``. + + .. WARNING:: + + The requirements of :meth:`krylov_basis` on the input ``degrees`` + also apply here: if they are not met, the behaviour of this + method is undefined. + + REFERENCES: + + [Kai1980]_ (Section 6.7), [BecLab2000]_ , and [JNSV2017]_ (Section 7). + + INPUT: + + - ``M`` -- a square matrix of size equal to the number of columns of + ``self``. + - ``shifts`` -- list of ``self.nrows()`` integers (optional): row + priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + (optional). The `i`-th entry ``degrees[i]`` indicates that + ``self[i,:] * M**degrees[i]`` is known to be dependent on rows before + it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to + ``self.ncols()`` for all rows. Giving a single integer for + ``degrees`` is equivalent to giving a list with this integer repeated + ``self.nrows()`` times. + - ``var`` -- (optional) variable name for the returned univariate + polynomial matrix, which is square of size `m \times m`, where `m` is + ``self.nrows()``. If ``None``, the default is to return a constant `m + \times (m (d+1))` matrix which is the horizontal concatenation of the + `d+1` coefficient matrices of this polynomial matrix of degree `d`. + + OUTPUT: + + If ``var`` is ``None``, a ``self.nrows() x self.nrows()`` matrix ``P`` + in ``shifts``-Popov form. Otherwise, the corresponding ``self.nrows() + x (self.nrows() * (P.degree()+1))`` matrix ``C`` where ``P[i,j][k] == + C[i,m*k+j]``. + + .. SEEALSO:: + + :meth:`krylov_basis` computes a basis of the Krylov subspace of + ``self`` and `M`; :meth:`krylov_matrix` computes the Krylov matrix + (without discarding linearly redundant rows) of ``self`` and `M`. + EXAMPLES:: sage: R = GF(97) sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: E - [27 49 29] - [50 58 0] - [77 10 29] sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: M - [0 1 0] - [0 0 1] - [0 0 0] - sage: E.krylov_kernel_basis(M,var='x') + + This example corresponds to computing a basis of Hermite-Padé + approximants:: + + sage: P = E.krylov_kernel_basis(M, var='x') [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] + sage: xring. = P.base_ring() + sage: F = E * matrix(xring, [[1], [x], [x**2]]) + sage: P == F.minimal_approximant_basis(3, normal_form=True) + True + + Instead of a univariate polynomial matrix, one can also obtain a + constant matrix representing the same kernel elements:: + sage: E.krylov_kernel_basis(M) [82 76 0 40 0 0 1 0 0] [13 57 0 3 1 0 0 0 0] [96 96 1 0 0 0 0 0 0] + + Other shifts will lead to different kernel bases:: + sage: shifts = [0,3,6] - sage: E.krylov_kernel_basis(M,shifts=shifts,var='x') + sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] @@ -19879,12 +19970,13 @@ cdef class Matrix(Matrix1): [ 0 0 0 0 0 0 0 0 0 1 0 0] [70 1 0 72 0 0 60 0 0 0 0 0] [69 0 1 72 0 0 60 0 0 0 0 0] + sage: shifts = [3,0,2] - sage: E.krylov_kernel_basis(M,shifts=shifts,var='x') + sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] - sage: E.krylov_kernel_basis(M,shifts=shifts) + sage: E.krylov_kernel_basis(M, shifts=shifts) [ 1 79 0 0 49 0 0 26 0 0 0 0] [ 0 0 0 0 0 0 0 0 0 0 1 0] [ 0 78 1 0 49 0 0 26 0 0 0 0] @@ -19892,9 +19984,9 @@ cdef class Matrix(Matrix1): TESTS:: sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) - sage: basis = E.krylov_kernel_basis(M,var='x') + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: basis = E.krylov_kernel_basis(M, var='x') sage: basis_coeff = E.krylov_kernel_basis(M) sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff True From c0326a102616668fafcf2930876ea5bd23c4e634 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Thu, 28 Aug 2025 00:13:24 +0200 Subject: [PATCH 40/57] documentation for krylov_kernel_basis --- src/sage/matrix/matrix2.pyx | 65 ++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 24d5872bbd8..36a091e836f 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19248,12 +19248,12 @@ cdef class Matrix(Matrix1): - ``shifts`` -- list of ``self.nrows()`` integers (optional), row priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers - (optional). The `i`-th entry ``degrees[i]`` indicates the number of - Krylov iterates to appear in the output (that is, ``self[i,:] * - M**j`` will appear for `j` up to ``degrees[i]``, included). If - ``None``, defaults to ``self.ncols()`` for all rows. Giving a single - integer for ``degrees`` is equivalent to giving a list with this - integer repeated ``self.nrows()`` times. + (optional). The entry ``degrees[i]`` indicates the number of Krylov + iterates to appear in the output (that is, ``self[i,:] * M**j`` will + appear for `j` up to ``degrees[i]``, included). If ``None``, defaults + to ``self.ncols()`` for all rows. Giving a single integer for + ``degrees`` is equivalent to giving a list with this integer repeated + ``self.nrows()`` times. OUTPUT: @@ -19679,9 +19679,9 @@ cdef class Matrix(Matrix1): - ``shifts`` -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers - (optional). The `i`-th entry ``degrees[i]`` indicates that - ``self[i,:] * M**degrees[i]`` is known to be dependent on rows before - it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to + (optional). The entry ``degrees[i]`` indicates that ``self[i,:] * + M**degrees[i]`` is known to be dependent on rows before it in the + ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. @@ -19849,15 +19849,15 @@ cdef class Matrix(Matrix1): def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): r""" - Return a Krylov kernel basis for (``self``,``M``) in canonical - form, depending on the row priority defined by ``shifts``. + Return a basis in canonical form for the kernel of the Krylov matrix of + ``(self, M)`` with rows ordered according to ``shifts``. Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters `M`, ``shifts``, and ``degrees``. Let `[\delta_0,\ldots,\delta_{m-1}]` be the exponents of first linear dependency for each row. That is, if the row `E_{i,:}` has not been selected for appearing in `B` then - `\delta_i = 0`, and otherwise, `delta_i` is such that `E_{i,:} \cdot + `\delta_i = 0`, and otherwise, `\delta_i` is such that `E_{i,:} \cdot M^{\delta_i-1}` has been selected for `B` but `E_{i,:} \cdot M^{\delta_i}` has not. (These integers are also easily deduced from the output triplets of :meth:`krylov_basis`.) @@ -19865,7 +19865,7 @@ cdef class Matrix(Matrix1): The returned matrix `K` is a basis, in reduced row echelon form, of the left nullspace of the Krylov matrix built from `E` and `M` with ``shifts`` and ``degrees`` equal to `[\delta_0,\ldots,\delta_{m-1}]`. - Recall from :meth:`krylov_matrix`` that this matrix is, up to a row + Recall from :meth:`krylov_matrix` that this matrix is, up to a row permutation indicated by ``shifts``, equal to .. MATH:: @@ -19877,19 +19877,19 @@ cdef class Matrix(Matrix1): E_{m-1,:} M^{\delta_{m-1}} \end{bmatrix}. - Since `B` has full row rank `r`, this kernel basis `K` has `m` rows and - `m+r` columns (where `r \le n`), and has rank `m`. Thanks to the module - structure underlying Krylov matrices, from the matrix `K` one can - deduce for free (up to copying rows and shifting coefficients to the - right) a basis of the left kernel bases for Krylov matrices that would - be built from the same `E`, `M`, and ``shifts``, but with larger - degrees, that is `[d_0,\ldots,d_{m-1}]` with `d_i \ge \delta_i` for all - `i`. + Since `B` has full row rank `r` (where `r \le n`), this kernel basis + `K` has `m` rows and `m+r` columns, and has rank `m`. This matrix `K` + can also be represented as a nonsingular univariate polynomial matrix + of size `m \times m` in ``shifts``-Popov form (see + :meth:`sage.matrix.matrix_polynomial_dense.Matrix_polynomial_dense.popov_form` + for definitions). This is provided as an option via the input ``var``. - This matrix `K` can also be represented as a nonsingular univariate - polynomial matrix of size `m \times m` in ``shifts``-Popov form (see - :meth:`sage.matrix.matrix_polynomial_dense.popov_form` for - definitions). This is provided as an option via the input ``var``. + Note that thanks to the module structure underlying Krylov matrices, + from the matrix `K` one can deduce for free (up to copying rows and + shifting coefficients to the right) left kernel bases for Krylov + matrices that would be built from the same `E`, `M`, and ``shifts``, + but with larger degrees, that is `[d_0,\ldots,d_{m-1}]` with `d_i \ge + \delta_i` for all `i`. .. WARNING:: @@ -19908,9 +19908,9 @@ cdef class Matrix(Matrix1): - ``shifts`` -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers - (optional). The `i`-th entry ``degrees[i]`` indicates that - ``self[i,:] * M**degrees[i]`` is known to be dependent on rows before - it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to + (optional). The entry ``degrees[i]`` indicates that ``self[i,:] * + M**degrees[i]`` is known to be dependent on rows before it in the + ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. @@ -19943,11 +19943,16 @@ cdef class Matrix(Matrix1): approximants:: sage: P = E.krylov_kernel_basis(M, var='x') + sage: P [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] - sage: xring. = P.base_ring() - sage: F = E * matrix(xring, [[1], [x], [x**2]]) + sage: x = P.base_ring().gen() + sage: F = E * matrix([[1], [x], [x**2]]) + sage: F + [29*x^2 + 49*x + 27] + [ 58*x + 50] + [29*x^2 + 10*x + 77] sage: P == F.minimal_approximant_basis(3, normal_form=True) True From 5e74fcddf865ad321222b33d13af016e4ed6ef13 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Thu, 28 Aug 2025 00:26:17 +0200 Subject: [PATCH 41/57] reintroduce conftest.py --- conftest.py | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 conftest.py diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000000..98fc948afa7 --- /dev/null +++ b/conftest.py @@ -0,0 +1,343 @@ +# pyright: strict +"""Configuration and fixtures for pytest. + +This file configures pytest and provides some global fixtures. +See https://docs.pytest.org/en/latest/index.html for more details. +""" + +from __future__ import annotations + +import doctest +import inspect +import sys +import warnings +from pathlib import Path +from typing import Any, Iterable, Optional + +import pytest +from _pytest.doctest import ( + DoctestItem, + DoctestModule, + _get_continue_on_failure, + _get_runner, + _is_mocked, + _patch_unwrap_mock_aware, + get_optionflags, +) +from _pytest.pathlib import ImportMode, import_path + +from sage.doctest.forker import ( + init_sage, + showwarning_with_traceback, +) +from sage.doctest.parsing import SageDocTestParser, SageOutputChecker + + +class SageDoctestModule(DoctestModule): + """ + This is essentially a copy of `DoctestModule` from + https://github.com/pytest-dev/pytest/blob/main/src/_pytest/doctest.py. + The only change is that we use `SageDocTestParser` to extract the doctests + and `SageOutputChecker` to verify the output. + """ + + def collect(self) -> Iterable[DoctestItem]: + import doctest + + class MockAwareDocTestFinder(doctest.DocTestFinder): + """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. + https://github.com/pytest-dev/pytest/issues/3456 + https://bugs.python.org/issue25532 + """ + + def __init__(self) -> None: + super().__init__(parser=SageDocTestParser(set(["sage"]))) + + def _find_lineno(self, obj, source_lines): + """Doctest code does not take into account `@property`, this + is a hackish way to fix it. https://bugs.python.org/issue17446 + Wrapped Doctests will need to be unwrapped so the correct + line number is returned. This will be reported upstream. #8796 + """ + if isinstance(obj, property): + obj = getattr(obj, "fget", obj) + + if hasattr(obj, "__wrapped__"): + # Get the main obj in case of it being wrapped + obj = inspect.unwrap(obj) + + # Type ignored because this is a private function. + return super()._find_lineno( # type:ignore[misc] + obj, + source_lines, + ) + + def _find( + self, tests, obj, name, module, source_lines, globs, seen + ) -> None: + if _is_mocked(obj): + return + with _patch_unwrap_mock_aware(): + # Type ignored because this is a private function. + super()._find( # type:ignore[misc] + tests, obj, name, module, source_lines, globs, seen + ) + + if self.path.name == "conftest.py": + module = self.config.pluginmanager._importconftest( + self.path, + self.config.getoption("importmode"), + rootpath=self.config.rootpath, + consider_namespace_packages=True, + ) + else: + try: + module = import_path( + self.path, + mode=ImportMode.importlib, + root=self.config.rootpath, + consider_namespace_packages=True, + ) + except ImportError as exception: + if self.config.getvalue("doctest_ignore_import_errors"): + pytest.skip("unable to import module %r" % self.path) + else: + if isinstance(exception, ModuleNotFoundError): + # Ignore some missing features/modules for now + # TODO: Remove this once all optional things are using Features + if exception.name in ( + "valgrind", + "rpy2", + "sage.libs.coxeter3.coxeter", + ): + pytest.skip( + f"unable to import module { self.path } due to missing feature { exception.name }" + ) + raise + # Uses internal doctest module parsing mechanism. + finder = MockAwareDocTestFinder() + optionflags = get_optionflags(self.config) + from sage.features import FeatureNotPresentError + + runner = _get_runner( + verbose=False, + optionflags=optionflags, + checker=SageOutputChecker(), + continue_on_failure=_get_continue_on_failure(self.config), + ) + try: + for test in finder.find(module, module.__name__): + if test.examples: # skip empty doctests + yield DoctestItem.from_parent( + self, name=test.name, runner=runner, dtest=test + ) + except FeatureNotPresentError as exception: + pytest.skip( + f"unable to import module { self.path } due to missing feature { exception.feature.name }" + ) + except ModuleNotFoundError as exception: + # TODO: Remove this once all optional things are using Features + pytest.skip( + f"unable to import module { self.path } due to missing module { exception.name }" + ) + + +class IgnoreCollector(pytest.Collector): + """ + Ignore a file. + """ + + def __init__(self, parent: pytest.Collector) -> None: + super().__init__("ignore", parent) + + def collect(self) -> Iterable[pytest.Item | pytest.Collector]: + return [] + + +def pytest_collect_file( + file_path: Path, parent: pytest.Collector +) -> pytest.Collector | None: + """ + This hook is called when collecting test files, and can be used to + modify the file or test selection logic by returning a list of + ``pytest.Item`` objects which the ``pytest`` command will directly + add to the list of test items. + + See `pytest documentation `_. + """ + if ( + file_path.parent.name == "combinat" + or file_path.parent.parent.name == "combinat" + ): + # Crashes CI for some reason + return IgnoreCollector.from_parent(parent) + if file_path.suffix == ".pyx": + # We don't allow pytests to be defined in Cython files. + # Normally, Cython files are filtered out already by pytest and we only + # hit this here if someone explicitly runs `pytest some_file.pyx`. + return IgnoreCollector.from_parent(parent) + elif file_path.suffix == ".py": + if parent.config.option.doctest: + if file_path.name == "__main__.py" or file_path.name == "setup.py": + # We don't allow tests to be defined in __main__.py/setup.py files (because their import will fail). + return IgnoreCollector.from_parent(parent) + if ( + ( + file_path.name == "postprocess.py" + and file_path.parent.name == "nbconvert" + ) + or ( + file_path.name == "giacpy-mkkeywords.py" + and file_path.parent.name == "autogen" + ) + or ( + file_path.name == "flint_autogen.py" + and file_path.parent.name == "autogen" + ) + ): + # This is an executable file. + return IgnoreCollector.from_parent(parent) + + if ( + ( + file_path.name == "finite_dimensional_lie_algebras_with_basis.py" + and file_path.parent.name == "categories" + ) + or ( + file_path.name == "__init__.py" + and file_path.parent.name == "crypto" + ) + or (file_path.name == "__init__.py" and file_path.parent.name == "mq") + ): + # TODO: Fix these (import fails with "RuntimeError: dictionary changed size during iteration") + return IgnoreCollector.from_parent(parent) + + if ( + file_path.name in ("forker.py", "reporting.py") + ) and file_path.parent.name == "doctest": + # Fails with many errors due to different testing framework + return IgnoreCollector.from_parent(parent) + + if ( + ( + file_path.name == "arithgroup_generic.py" + and file_path.parent.name == "arithgroup" + ) + or ( + file_path.name == "pari.py" + and file_path.parent.name == "lfunctions" + ) + or ( + file_path.name == "permgroup_named.py" + and file_path.parent.name == "perm_gps" + ) + or ( + file_path.name == "finitely_generated.py" + and file_path.parent.name == "matrix_gps" + ) + or ( + file_path.name == "libgap_mixin.py" + and file_path.parent.name == "groups" + ) + or ( + file_path.name == "finitely_presented.py" + and file_path.parent.name == "groups" + ) + or ( + file_path.name == "classical_geometries.py" + and file_path.parent.name == "generators" + ) + ): + # Fails with "Fatal Python error" + return IgnoreCollector.from_parent(parent) + + return SageDoctestModule.from_parent(parent, path=file_path) + + +def pytest_addoption(parser): + # Add a command line option to run doctests + # (we don't use the built-in --doctest-modules option because then doctests are collected twice) + group = parser.getgroup("collect") + group.addoption( + "--doctest", + action="store_true", + default=False, + help="Run doctests in all .py modules", + dest="doctest", + ) + + +# Monkey patch exception printing to replace the full qualified name of the exception by its short name +# TODO: Remove this hack once migration to pytest is complete +import traceback + +old_format_exception_only = traceback.format_exception_only + + +def format_exception_only(etype: type, value: BaseException) -> list[str]: + formatted_exception = old_format_exception_only(etype, value) + exception_name = etype.__name__ + if etype.__module__: + exception_full_name = etype.__module__ + "." + etype.__qualname__ + else: + exception_full_name = etype.__qualname__ + + for i, line in enumerate(formatted_exception): + if line.startswith(exception_full_name): + formatted_exception[i] = line.replace( + exception_full_name, exception_name, 1 + ) + return formatted_exception + + +# Initialize Sage-specific doctest stuff +init_sage() + +# Monkey patch doctest to use our custom printer etc +old_run = doctest.DocTestRunner.run + + +def doctest_run( + self: doctest.DocTestRunner, + test: doctest.DocTest, + compileflags: Optional[int] = None, + out: Any = None, + clear_globs: bool = True, +) -> doctest.TestResults: + from sage.repl.rich_output import get_display_manager + from sage.repl.user_globals import set_globals + + traceback.format_exception_only = format_exception_only + + # Display warnings in doctests + warnings.showwarning = showwarning_with_traceback + setattr(sys, "__displayhook__", get_display_manager().displayhook) + + # Ensure that injecting globals works as expected in doctests + set_globals(test.globs) + return old_run(self, test, compileflags, out, clear_globs) + + +doctest.DocTestRunner.run = doctest_run + + +@pytest.fixture(autouse=True, scope="session") +def add_imports(doctest_namespace: dict[str, Any]): + """ + Add global imports for doctests. + + See `pytest documentation `. + """ + # Inject sage.all into each doctest + import sage.repl.ipython_kernel.all_jupyter + + dict_all = sage.repl.ipython_kernel.all_jupyter.__dict__ + + # Remove '__package__' item from the globals since it is not + # always in the globals in an actual Sage session. + dict_all.pop("__package__", None) + + sage_namespace = dict(dict_all) + sage_namespace["__name__"] = "__main__" + + doctest_namespace.update(**sage_namespace) From 956dd3b0abb8c493f9c1225dc5ace40c44877235 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Thu, 28 Aug 2025 18:29:43 +0200 Subject: [PATCH 42/57] documentation fixes --- src/sage/matrix/matrix2.pyx | 63 +++---------------------------------- 1 file changed, 4 insertions(+), 59 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 36a091e836f..0736ddd4512 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19125,7 +19125,7 @@ cdef class Matrix(Matrix1): def _krylov_row_coordinates(self, shifts, degrees, row_pairs=None): r""" Helper function for :meth:`krylov_matrix` and :meth:`krylov_basis`. - Returns a sorted set of triplets corresponding to row coordinates, for + Return a sorted set of triplets corresponding to row coordinates, for a given set of shifts and degrees. INPUT: @@ -19293,43 +19293,9 @@ cdef class Matrix(Matrix1): [ 0 0 0] [ 0 0 0] - Row ordering for the zero shift:: - - sage: degrees = 3 # default in above constructions - sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) - ....: for i in range(E.nrows())] - sage: rows - [(0, 0, 0), - (1, 0, 1), - (2, 0, 2), - (0, 1, 3), - (1, 1, 4), - (2, 1, 5), - (0, 2, 6), - (1, 2, 7), - (2, 2, 8), - (0, 3, 9), - (1, 3, 10), - (2, 3, 11)] - Trying the other above-mentioned classical shift:: sage: shifts = [0,3,6] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) - sage: rows - [(0, 0, 0), - (0, 1, 3), - (0, 2, 6), - (0, 3, 9), - (1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (1, 3, 10), - (2, 0, 2), - (2, 1, 5), - (2, 2, 8), - (2, 3, 11)] - sage: E.krylov_matrix(M, shifts) [27 49 29] [ 0 27 49] @@ -19344,25 +19310,9 @@ cdef class Matrix(Matrix1): [ 0 0 77] [ 0 0 0] - With another shift:: sage: shifts = [3,0,2] - sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) - sage: rows - [(1, 0, 1), - (1, 1, 4), - (1, 2, 7), - (2, 0, 2), - (0, 0, 0), - (1, 3, 10), - (2, 1, 5), - (0, 1, 3), - (2, 2, 8), - (0, 2, 6), - (2, 3, 11), - (0, 3, 9)] - sage: E.krylov_matrix(M, shifts) [50 58 0] [ 0 50 58] @@ -19504,8 +19454,6 @@ cdef class Matrix(Matrix1): See :meth:`krylov_basis` for the description of this method. However, unlike :meth:`krylov_basis`, this method performs no input validation. - EXAMPLES:: - TESTS:: sage: R = GF(97) @@ -19655,10 +19603,6 @@ cdef class Matrix(Matrix1): degree of the minimal polynomial of `M`. By default, the implementation takes ``degrees`` as `[n, \ldots, n]`. - This method incidentally computes the column rank profile (as returned - by :meth:`pivots`) of its output matrix, and so, it is cached in the - output for faster future access. - By default, the method also returns information that relate the computed basis rows to the corresponding rows in the Krylov matrix `K` as if built with :meth:`krylov_matrix` with the same parameters. @@ -19685,8 +19629,9 @@ cdef class Matrix(Matrix1): ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``output_rows`` -- determines whether information relating the output - rows to their position in the Krylov matrix is also provided. + - ``output_rows`` -- boolean, optional, defaults to True. Determines + whether information relating the output rows to their position in the + Krylov matrix is also provided. - ``algorithm`` -- either 'naive', 'elimination', or None (let Sage choose). From 7012b946be3f22b840e101e5913a9dbce2d78d8e Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Thu, 28 Aug 2025 19:01:02 +0200 Subject: [PATCH 43/57] documentation fixes --- src/sage/matrix/matrix2.pyx | 64 ++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 0736ddd4512..97b13eb74d1 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19196,12 +19196,12 @@ cdef class Matrix(Matrix1): r""" Return the Krylov matrix built from the rows of ``self`` and using multiplication matrix `M`. Here, ``self`` is some `m \times n` matrix - `E`, and `M` is a square `n \times n` matrix; ``degrees`` is a list of - `m` nonnegative integers `[d_0,\ldots,d_{m-1}]`. This Krylov matrix has - `d_0+\cdots+d_{m-1}+m` rows and `n` columns and is formed by stacking - the Krylov iterates `E_{i,:} \cdot M^j` for all `0 \le i < m` and `0 - \le j \le d_i`. These rows are ordered according to a priority defined - by ``shifts``. + `E` with rows denoted by `E_0, \ldots, E_{m-1}`, and `M` is a square `n + \times n` matrix; ``degrees`` is a list of `m` nonnegative integers + `[d_0,\ldots,d_{m-1}]`. This Krylov matrix has `d_0+\cdots+d_{m-1}+m` + rows and `n` columns and is formed by stacking the Krylov iterates + `E_{i} \cdot M^j` for all `0 \le i < m` and `0 \le j \le d_i`. These + rows are ordered according to a priority defined by ``shifts``. By default, ``shifts`` is taken as `[0,\ldots,0]`, and ``degrees`` is taken as `[n, \ldots, n]`. If a single integer `d` is provided for @@ -19227,15 +19227,15 @@ cdef class Matrix(Matrix1): .. MATH:: \begin{bmatrix} - E_{0,:} \\ - E_{0,:} M \\ + E_0 \\ + E_0 M \\ \vdots \\ - E_{0,:} M^{d_0} \\ + E_0 M^{d_0} \\ \vdots \\ - E_{m-1,:} \\ - E_{m-1,:} M \\ + E_{m-1} \\ + E_{m-1} M \\ \vdots \\ - E_{m-1,:} M^{d_{m-1}} + E_{m-1} M^{d_{m-1}} \end{bmatrix} . Other shifts will yield some row permutation of the latter matrix (see @@ -19587,17 +19587,18 @@ cdef class Matrix(Matrix1): def krylov_basis(self, M, shifts=None, degrees=None, output_rows=True, algorithm=None): r""" Return the matrix `B` formed by stacking the first `r` linearly - independent row vectors `E_{i,:} \cdot M^j`, for `0 \le i < m` and `j - \ge 0` when they are ordered according to ``shifts``. Here `E` is the - input `m \times n` matrix ``self``, `M` is a square `n \times n` - matrix, and `r` is dimension of Krylov subspace of `E` and `M`, that - is, the span of all the above row vectors. + independent row vectors `E_{i} \cdot M^j`, for `0 \le i < m` and `j \ge + 0` when they are ordered according to ``shifts``. Here `E` is the input + `m \times n` matrix ``self``, with rows denoted by `E_0, \ldots, + E_{m-1}`, `M` is a square `n \times n` matrix, and `r` is dimension of + Krylov subspace of `E` and `M`, that is, the span of all the above row + vectors. A list ``degrees`` of integers `[d_0,\ldots,d_{m-1}]` can be provided - to indicate known maximal exponents, such that `E_{k,:} \cdot - M^{d_k-1}` may appear in the output basis, but `E_{k,:} \cdot - M^{d_k}` will not. In other words, `E_{k,:} \cdot M^{d_k}` is known to - linearly depend on the set of row vectors `E_{i,:} \cdot M^{j}` that + to indicate known maximal exponents, such that `E_{k} \cdot + M^{d_k-1}` may appear in the output basis, but `E_{k} \cdot + M^{d_k}` will not. In other words, `E_{k} \cdot M^{d_k}` is known to + linearly depend on the set of row vectors `E_{i} \cdot M^{j}` that appear before it according to the order defined by ``shifts``. It is always valid to take ``degrees`` as `[d, \ldots, d]` where `d` is the degree of the minimal polynomial of `M`. By default, the implementation @@ -19608,7 +19609,7 @@ cdef class Matrix(Matrix1): as if built with :meth:`krylov_matrix` with the same parameters. Specifically, for each row of the output, this information gives its position in `K` (thus forming the row rank profile of `K`), as well as - pairs `i, j` indicating that the row is `E_{i,:} \cdot M^{j}`. + pairs `i, j` indicating that the row is `E_{i} \cdot M^{j}`. .. WARNING:: @@ -19795,17 +19796,20 @@ cdef class Matrix(Matrix1): def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): r""" Return a basis in canonical form for the kernel of the Krylov matrix of - ``(self, M)`` with rows ordered according to ``shifts``. + ``(self, M)`` with rows ordered according to ``shifts``. In other terms, + the rows of the returned matrix form a basis of the kernel of the + `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to K^n` given by the matrix + `E`, where the action of `x` on `K^n` is given by `M`. Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters `M`, ``shifts``, and ``degrees``. Let `[\delta_0,\ldots,\delta_{m-1}]` be the exponents of first linear dependency for each row. That is, if - the row `E_{i,:}` has not been selected for appearing in `B` then - `\delta_i = 0`, and otherwise, `\delta_i` is such that `E_{i,:} \cdot - M^{\delta_i-1}` has been selected for `B` but `E_{i,:} \cdot - M^{\delta_i}` has not. (These integers are also easily deduced from the - output triplets of :meth:`krylov_basis`.) + the `i`-th row `E_i` of `E` has not been selected for appearing in `B` + then `\delta_i = 0`, and otherwise, `\delta_i` is such that `E_i \cdot + M^{\delta_i-1}` has been selected for `B` but `E_i \cdot M^{\delta_i}` + has not. (These integers are also easily deduced from the output + triplets of :meth:`krylov_basis`.) The returned matrix `K` is a basis, in reduced row echelon form, of the left nullspace of the Krylov matrix built from `E` and `M` with @@ -19817,9 +19821,9 @@ cdef class Matrix(Matrix1): \begin{bmatrix} B \\ - E_{0,:} M^{\delta_0} \\ + E_0 M^{\delta_0} \\ \vdots \\ - E_{m-1,:} M^{\delta_{m-1}} + E_{m-1} M^{\delta_{m-1}} \end{bmatrix}. Since `B` has full row rank `r` (where `r \le n`), this kernel basis From 2cd43c900868b8887016a4f682c5dfed102c3302 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 28 Aug 2025 14:10:44 +0200 Subject: [PATCH 44/57] replace math.ceil and math.log --- src/sage/matrix/matrix2.pyx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 97b13eb74d1..9b97ab9b68b 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19369,6 +19369,7 @@ cdef class Matrix(Matrix1): raise ValueError(f"krylov_matrix: shifts is not an integer vector of length {E.nrows()}.") m = E.nrows() + iterations = max(degrees, default=ZZ.zero()).nbits() # store 2 blocks for the Krylov matrix blocks = [E] @@ -19378,7 +19379,7 @@ cdef class Matrix(Matrix1): # for i from 0 to degree (inclusive), merge existing blocks, add E*M^i M_i = None - for i in range(math.ceil(math.log(max(degrees, default=0) + 1, 2))): + for i in range(iterations): if M_i is None: M_i = M else: @@ -19488,6 +19489,7 @@ cdef class Matrix(Matrix1): m = self.nrows() sigma = self.ncols() + iterations = max(degrees, default=ZZ.zero()).nbits() if m == 0: return (self, ()) if output_rows else self @@ -19513,7 +19515,7 @@ cdef class Matrix(Matrix1): M_L = None - for l in range(math.ceil(math.log(max(degrees, default=0) + 1, 2))): + for l in range(iterations): L = pow(2, l) # adding 2^l to each degree row_extension = [(x[0], x[1] + L) for x in row_profile_self if x[1] + L <= degrees[x[0]]] @@ -19543,7 +19545,7 @@ cdef class Matrix(Matrix1): row_profile_R = R.pivot_rows() r = len(row_profile_R) - if r == sigma and l < math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: + if r == sigma and l < iterations - 1: tail = list(range(row_profile_R[-1]+1,R.nrows())) excluded_rows.update(set([k[k_rows[i]][0] for i in tail if i < len(k)])) @@ -19556,7 +19558,7 @@ cdef class Matrix(Matrix1): exhausted = R.matrix_from_rows(xmi) R = R.matrix_from_rows(imi) else: - if l == math.ceil(math.log(max(degrees, default=0) + 1, 2)) - 1: + if l == iterations - 1: row_profile_exhausted = [] exhausted = matrix.zero(self.base_ring(), 0, sigma) row_profile_self = [k[k_rows[i]] for i in row_profile_R] @@ -19639,7 +19641,7 @@ cdef class Matrix(Matrix1): OUTPUT: - matrix formed by the first `r` independent rows of the Krylov matrix - of ``self`` and `M`, with `r` the rank of that matrix + of ``self`` and `M`, with `r` the rank of that matrix - ``row_profile`` (returned if ``output_rows`` is ``True``): list of the ``r`` triplets ``(i, j, k)`` corresponding to the rows of the Krylov basis where ``k`` is the row index of the row in the Krylov From 174c4ba8b75336b2061d10b49e576ed2575a7978 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Fri, 29 Aug 2025 09:16:04 +0200 Subject: [PATCH 45/57] Update src/sage/matrix/matrix2.pyx Co-authored-by: Xavier Caruso --- src/sage/matrix/matrix2.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 97b13eb74d1..3c571faf6dc 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19799,7 +19799,7 @@ cdef class Matrix(Matrix1): ``(self, M)`` with rows ordered according to ``shifts``. In other terms, the rows of the returned matrix form a basis of the kernel of the `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to K^n` given by the matrix - `E`, where the action of `x` on `K^n` is given by `M`. + ``self``, where the action of `x` on `K^n` is given by `M`. Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters From 6f13c15f50657f87c664b5eda74a2d9972978229 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 29 Aug 2025 17:25:37 +0200 Subject: [PATCH 46/57] change coefficient matrix output --- src/sage/matrix/matrix2.pyx | 111 +++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 9b97ab9b68b..a30aabec01c 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19867,9 +19867,9 @@ cdef class Matrix(Matrix1): ``self.nrows()`` times. - ``var`` -- (optional) variable name for the returned univariate polynomial matrix, which is square of size `m \times m`, where `m` is - ``self.nrows()``. If ``None``, the default is to return a constant `m - \times (m (d+1))` matrix which is the horizontal concatenation of the - `d+1` coefficient matrices of this polynomial matrix of degree `d`. + ``self.nrows()``. If ``None``, the default is to return a matrix of + coefficients as described above, along with the list of row + coordinates corresponding to the columns of the matrix. OUTPUT: @@ -19911,9 +19911,12 @@ cdef class Matrix(Matrix1): constant matrix representing the same kernel elements:: sage: E.krylov_kernel_basis(M) - [82 76 0 40 0 0 1 0 0] - [13 57 0 3 1 0 0 0 0] - [96 96 1 0 0 0 0 0 0] + ( + [82 76 0 40 0 1] + [13 57 0 3 1 0] + [96 96 1 0 0 0], + ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) + ) Other shifts will lead to different kernel bases:: @@ -19923,9 +19926,12 @@ cdef class Matrix(Matrix1): [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] sage: E.krylov_kernel_basis(M,shifts=shifts) - [ 0 0 0 0 0 0 0 0 0 1 0 0] - [70 1 0 72 0 0 60 0 0 0 0 0] - [69 0 1 72 0 0 60 0 0 0 0 0] + ( + [ 0 0 0 1 0 0] + [70 72 60 0 1 0] + [69 72 60 0 0 1], + ((0, 0, 0), (0, 1, 1), (0, 2, 2), (0, 3, 3), (1, 0, 4), (2, 0, 8)) + ) sage: shifts = [3,0,2] sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') @@ -19933,9 +19939,12 @@ cdef class Matrix(Matrix1): [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] sage: E.krylov_kernel_basis(M, shifts=shifts) - [ 1 79 0 0 49 0 0 26 0 0 0 0] - [ 0 0 0 0 0 0 0 0 0 0 1 0] - [ 0 78 1 0 49 0 0 26 0 0 0 0] + ( + [79 49 26 0 1 0] + [ 0 0 0 0 0 1] + [78 49 26 1 0 0], + ((1, 0, 0), (1, 1, 1), (1, 2, 2), (2, 0, 3), (0, 0, 4), (1, 3, 5)) + ) TESTS:: @@ -19944,50 +19953,49 @@ cdef class Matrix(Matrix1): sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) sage: basis = E.krylov_kernel_basis(M, var='x') sage: basis_coeff = E.krylov_kernel_basis(M) - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False) + sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True - sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) + sage: krylov_matrix = E.krylov_matrix(M,degrees=[basis[i,i].degree() for i in range(E.nrows())]) sage: basis.is_popov() True sage: basis.degree() <= E.ncols() True - sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() - True - sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) + sage: basis_coeff[0] * krylov_matrix == 0 True sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shifts = [0,3,6] - sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') - sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + sage: basis = E.krylov_kernel_basis(M, shifts=shifts, var='x') + sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) + sage: basis_coeff = E.krylov_kernel_basis(M, shifts=shifts) + sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) + sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True - sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) + sage: krylov_matrix = E.krylov_matrix(M,shifts=shifts,degrees=[basis[i,i].degree() for i in range(E.nrows())]) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True - sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() + sage: basis_coeff[0] * krylov_matrix == 0 True - sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) - True - sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M, shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shifts = [3,0,2] - sage: basis = E.krylov_kernel_basis(M,shifts=shifts,var='x') - sage: basis_coeff = E.krylov_kernel_basis(M,shifts=shifts) - sage: matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]]) == basis_coeff + sage: basis = E.krylov_kernel_basis(M, shifts=shifts, var='x') + sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) + sage: basis_coeff = E.krylov_kernel_basis(M, shifts=shifts) + sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) + sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True - sage: krylov_matrix = E.krylov_matrix(M,degrees=max(basis.degree(),0)) + sage: krylov_matrix = E.krylov_matrix(M,shifts=shifts,degrees=[basis[i,i].degree() for i in range(E.nrows())]) sage: basis.is_popov(shifts=shifts) True sage: basis.degree() <= E.ncols() True - sage: (max(basis.degree(),0) + 1) * E.nrows() == basis_coeff.ncols() + sage: basis_coeff[0] * krylov_matrix == 0 True - sage: basis_coeff * krylov_matrix == matrix.zero(R,E.nrows()) - True - sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + sage: len(E.krylov_basis(M, shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True """ from sage.combinat.permutation import Permutation @@ -20030,14 +20038,27 @@ cdef class Matrix(Matrix1): m = E.nrows() sigma = E.ncols() base_ring = E.base_ring() + if var is None: + # convert c,d to actual position in striped Krylov matrix + phi = lambda row,deg : sum(min(max(shifts[row] - shifts[i] + deg + (i < row and shifts[i] <= shifts[row] + deg), 0), degrees[i] + 1) for i in range(m)) # calculate krylov profile krylov_basis, row_profile = E.krylov_basis(M, shifts, degrees) col_profile = krylov_basis.pivots() if len(row_profile) == 0: - ring = base_ring if var is None else PolynomialRing(base_ring, var) - return matrix.identity(ring, m) + if var is None: + coefficients = matrix.identity(base_ring, m) + + row_coords_coeff = E._krylov_row_coordinates(shifts, degrees, [(i, 0) for i in range(m)]) + permutation = Permutation([x[2]+1 for x in row_coords_coeff]) + coefficients.permute_columns(permutation) + + row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) + + return coefficients, row_coords_krylov + else: + return matrix.identity(PolynomialRing(base_ring, var), m) c, d, _ = zip(*(row for row in row_profile)) @@ -20069,17 +20090,15 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() if var is None: - # construct coefficient matrix - coefficients = matrix.zero(base_ring,m,m*(max(degree_c, default=0)+1)) - # add identity part of kernel basis - for i in range(m): - coefficients[i,i+degree_c[i]*m] = base_ring.one() - # add relation part of kernel basis - for col in range(relation.ncols()): - for row in range(m): - coefficients[row,c[col]+d[col]*m] -= relation[row,col] - - return coefficients + coefficients = (-relation).augment(matrix.identity(base_ring,m)) + rows = list(zip(c, d)) + list(enumerate(degree_c)) + row_coords_coeff = E._krylov_row_coordinates(shifts,degrees,rows) + permutation = Permutation([x[2]+1 for x in row_coords_coeff]) + coefficients.permute_columns(permutation) + + row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) + + return coefficients, row_coords_krylov else: poly_ring = PolynomialRing(base_ring, var) From 54376b95934f65588b363dcf8e9e70c3ba8b22a2 Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Sat, 30 Aug 2025 18:25:12 +0200 Subject: [PATCH 47/57] improving doc + adding support for ``var`` --- src/sage/matrix/matrix2.pyx | 134 ++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 44 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 5acc6674da2..f338dec4806 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -65,6 +65,9 @@ AUTHORS: - Moritz Firsching(2020-10-05): added ``quantum_determinant`` - Dima Pasechnik (2022-11-08): fixed ``echelonize`` for inexact matrices + +- Nicholas Bell, Xavier Caruso, Vincent Neiger (2025-08-31): + added ``krylov_matrix``, ``krylov_basis``, ``krylov_kernel_basis`` """ # **************************************************************************** @@ -19795,7 +19798,7 @@ cdef class Matrix(Matrix1): else: raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, shifts=None, degrees=None, var=None): + def krylov_kernel_basis(self, M, shifts=None, degrees=None, output_rows=True, var=None): r""" Return a basis in canonical form for the kernel of the Krylov matrix of ``(self, M)`` with rows ordered according to ``shifts``. In other terms, @@ -19814,10 +19817,10 @@ cdef class Matrix(Matrix1): triplets of :meth:`krylov_basis`.) The returned matrix `K` is a basis, in reduced row echelon form, of the - left nullspace of the Krylov matrix built from `E` and `M` with + left nullspace of the Krylov matrix `A` built from `E` and `M` with ``shifts`` and ``degrees`` equal to `[\delta_0,\ldots,\delta_{m-1}]`. - Recall from :meth:`krylov_matrix` that this matrix is, up to a row - permutation indicated by ``shifts``, equal to + Recall from :meth:`krylov_matrix` that this matrix `A` is, up to a row + permutation deduced from ``shifts``, equal to .. MATH:: @@ -19831,9 +19834,15 @@ cdef class Matrix(Matrix1): Since `B` has full row rank `r` (where `r \le n`), this kernel basis `K` has `m` rows and `m+r` columns, and has rank `m`. This matrix `K` can also be represented as a nonsingular univariate polynomial matrix - of size `m \times m` in ``shifts``-Popov form (see + `P` of size `m \times m` in ``shifts``-Popov form (see :meth:`sage.matrix.matrix_polynomial_dense.Matrix_polynomial_dense.popov_form` - for definitions). This is provided as an option via the input ``var``. + for definitions), whose sum of column degrees is `r`, as well as its + degree of determinant. This is provided as an option via the input + ``var``. + + By default, the method also returns information that relate the + computed kernel basis columns to the corresponding rows in the Krylov + matrix `A` (see the output specification). Note that thanks to the module structure underlying Krylov matrices, from the matrix `K` one can deduce for free (up to copying rows and @@ -19865,18 +19874,28 @@ cdef class Matrix(Matrix1): ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``var`` -- (optional) variable name for the returned univariate - polynomial matrix, which is square of size `m \times m`, where `m` is - ``self.nrows()``. If ``None``, the default is to return a matrix of - coefficients as described above, along with the list of row - coordinates corresponding to the columns of the matrix. + - ``output_rows`` -- boolean, optional, defaults to True. Determines + whether information relating the output columns to the rows of the + corresponding Krylov matrix is also provided. + - ``var`` -- (optional) Defaults to ``None``, which means returning a + constant matrix. If not ``None``, the polynomial matrix + representation is used, and ``var`` must be either the generator of + a univariate polynomial ring over the base ring of ``self``, or a + string which will be used as variable name for building such a + polynomial ring. OUTPUT: - If ``var`` is ``None``, a ``self.nrows() x self.nrows()`` matrix ``P`` - in ``shifts``-Popov form. Otherwise, the corresponding ``self.nrows() - x (self.nrows() * (P.degree()+1))`` matrix ``C`` where ``P[i,j][k] == - C[i,m*k+j]``. + - A matrix. If ``var`` is ``None``, this is the `m \times (m+r)` + constant matrix `K` described above. Otherwise this is the + corresponding `m \times m` univariate polynomial matrix `P` whose + variable is ``var``. + - ``row_profile`` (returned if ``output_rows`` is ``True``): list of + the ``m+r`` triplets ``(i, j, k)`` corresponding to the rows of the + Krylov matrix of which the returned matrix is a kernel basis. Here + ``k`` is the row index of the row in the Krylov matrix, ``i`` is the + corresponding row in ``self`` and ``j`` is the corresponding power of + ``M``. .. SEEALSO:: @@ -19887,13 +19906,13 @@ cdef class Matrix(Matrix1): EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R,[[27,49,29],[50,58,0],[77,10,29]]) - sage: M = matrix(R,[[0,1,0],[0,0,1],[0,0,0]]) + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) This example corresponds to computing a basis of Hermite-Padé approximants:: - sage: P = E.krylov_kernel_basis(M, var='x') + sage: P, row_profile = E.krylov_kernel_basis(M, var='x') sage: P [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] @@ -19910,7 +19929,9 @@ cdef class Matrix(Matrix1): Instead of a univariate polynomial matrix, one can also obtain a constant matrix representing the same kernel elements:: - sage: E.krylov_kernel_basis(M) + sage: K, row_profile_bis = E.krylov_kernel_basis(M) + sage: row_profile == row_profile_bis + sage: K, row_profile ( [82 76 0 40 0 1] [13 57 0 3 1 0] @@ -19918,26 +19939,35 @@ cdef class Matrix(Matrix1): ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) ) + Observe that the output Krylov matrix row profile also allows one to + switch between the polynomial representation and the constant one:: + + sage: all(P[:,j] == sum(K[:,k] * x**(row_profile[k][1]) + ....: for k in range(K.ncols()) + ....: if row_profile[k][0] == j) + ....: for j in range(P.ncols())) + True + Other shifts will lead to different kernel bases:: sage: shifts = [0,3,6] - sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') + sage: E.krylov_kernel_basis(M, shifts=shifts, output_rows=False, var='x') [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] - sage: E.krylov_kernel_basis(M,shifts=shifts) - ( + sage: E.krylov_kernel_basis(M, shifts=shifts, output_rows=False) [ 0 0 0 1 0 0] [70 72 60 0 1 0] - [69 72 60 0 0 1], - ((0, 0, 0), (0, 1, 1), (0, 2, 2), (0, 3, 3), (1, 0, 4), (2, 0, 8)) - ) + [69 72 60 0 0 1] sage: shifts = [3,0,2] sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') + ( [ 1 26*x^2 + 49*x + 79 0] [ 0 x^3 0] [ 0 26*x^2 + 49*x + 78 1] + ((1, 0, 0), (1, 1, 1), (1, 2, 2), (2, 0, 3), (0, 0, 4), (1, 3, 5)) + ) sage: E.krylov_kernel_basis(M, shifts=shifts) ( [79 49 26 0 1 0] @@ -19951,8 +19981,8 @@ cdef class Matrix(Matrix1): sage: R = GF(97) sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) - sage: basis = E.krylov_kernel_basis(M, var='x') - sage: basis_coeff = E.krylov_kernel_basis(M) + sage: basis, row_profile = E.krylov_kernel_basis(M, var='x') + sage: basis_coeff, row_profile_coeff = E.krylov_kernel_basis(M) sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False) sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True @@ -19966,9 +19996,9 @@ cdef class Matrix(Matrix1): sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shifts = [0,3,6] - sage: basis = E.krylov_kernel_basis(M, shifts=shifts, var='x') + sage: basis = E.krylov_kernel_basis(M, shifts=shifts, output_rows=False, var='x') sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) - sage: basis_coeff = E.krylov_kernel_basis(M, shifts=shifts) + sage: basis_coeff = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts) sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True @@ -19982,9 +20012,9 @@ cdef class Matrix(Matrix1): sage: len(E.krylov_basis(M, shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) True sage: shifts = [3,0,2] - sage: basis = E.krylov_kernel_basis(M, shifts=shifts, var='x') + sage: basis = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts, var='x') sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) - sage: basis_coeff = E.krylov_kernel_basis(M, shifts=shifts) + sage: basis_coeff = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts) sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] True @@ -20016,8 +20046,16 @@ cdef class Matrix(Matrix1): if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) - if var is not None and not isinstance(var, str): - raise TypeError("var is not a string") + if not (var is None or isinstance(var, str)): + generator = False + try: + generator = var.is_gen() + except AttributeError: + pass + if not generator: + raise TypeError('polynomial variable must be a string or univariate polynomial ring generator, not {0}'.format(var)) + elif var.base_ring() != E.base_ring(): + raise TypeError('polynomial generator must be over the same ring as the matrix entries') if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) @@ -20089,18 +20127,20 @@ cdef class Matrix(Matrix1): D = matrix(D_rows) relation = D*C.inverse() + rows = list(zip(c, d)) + list(enumerate(degree_c)) + row_coords_coeff = E._krylov_row_coordinates(shifts,degrees,rows) + row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) + if var is None: - coefficients = (-relation).augment(matrix.identity(base_ring,m)) - rows = list(zip(c, d)) + list(enumerate(degree_c)) - row_coords_coeff = E._krylov_row_coordinates(shifts,degrees,rows) + kkbasis = (-relation).augment(matrix.identity(base_ring,m)) permutation = Permutation([x[2]+1 for x in row_coords_coeff]) - coefficients.permute_columns(permutation) - - row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) - - return coefficients, row_coords_krylov + kkbasis.permute_columns(permutation) + else: - poly_ring = PolynomialRing(base_ring, var) + if isinstance(var, str): + poly_ring = PolynomialRing(base_ring, var) + else: + poly_ring = var.parent() # construct coefficient map coeffs_map = [[{} for _ in range(m)] for _ in range(m)] @@ -20112,8 +20152,14 @@ cdef class Matrix(Matrix1): for row in range(m): coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], base_ring.zero()) - relation[row,col] - # convert to matrix (slow for extension fields) - return matrix(poly_ring, m, m, coeffs_map) + # convert to matrix + # TODO slow for extension fields (2025-08-30), see Issue 40667 + kkbasis = matrix(poly_ring, m, m, coeffs_map) + + if output_rows: + return kkbasis, row_coords_krylov + else: + return kkbasis # a limited number of access-only properties are provided for matrices @property From eb8792821a2e72b4d9f59266b369af0310b8741a Mon Sep 17 00:00:00 2001 From: Vincent Neiger Date: Sat, 30 Aug 2025 22:43:55 +0200 Subject: [PATCH 48/57] add support for generator var; improve documentation and examples --- src/sage/matrix/matrix2.pyx | 233 +++++++++++++++++++++--------------- 1 file changed, 137 insertions(+), 96 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index f338dec4806..12fdb35b316 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19583,7 +19583,7 @@ cdef class Matrix(Matrix1): if not output_rows: return R - # convert c,d to actual position in striped Krylov matrix + # convert c,d to actual position in Krylov matrix phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degrees[i] + 1) for i in range(m)) row_profile = tuple([(*row, phi(*row)) for row in row_profile_self]) @@ -19816,11 +19816,12 @@ cdef class Matrix(Matrix1): has not. (These integers are also easily deduced from the output triplets of :meth:`krylov_basis`.) - The returned matrix `K` is a basis, in reduced row echelon form, of the - left nullspace of the Krylov matrix `A` built from `E` and `M` with - ``shifts`` and ``degrees`` equal to `[\delta_0,\ldots,\delta_{m-1}]`. - Recall from :meth:`krylov_matrix` that this matrix `A` is, up to a row - permutation deduced from ``shifts``, equal to + The returned matrix `K` is a basis, in reduced row echelon form (upper + echelon, and up to row permutation), of the left kernel of the Krylov + matrix `A` built from `E` and `M` with ``shifts`` and ``degrees`` equal + to `[\delta_0,\ldots,\delta_{m-1}]`. Recall from :meth:`krylov_matrix` + that this matrix `A` is, up to a row permutation deduced from + ``shifts``, equal to .. MATH:: @@ -19880,9 +19881,8 @@ cdef class Matrix(Matrix1): - ``var`` -- (optional) Defaults to ``None``, which means returning a constant matrix. If not ``None``, the polynomial matrix representation is used, and ``var`` must be either the generator of - a univariate polynomial ring over the base ring of ``self``, or a - string which will be used as variable name for building such a - polynomial ring. + a polynomial ring over the base ring of ``self``, or a string which + will be used as variable name for building such a polynomial ring. OUTPUT: @@ -19917,6 +19917,8 @@ cdef class Matrix(Matrix1): [x^2 + 40*x + 82 76 0] [ 3*x + 13 x + 57 0] [ 96 96 1] + sage: P.is_popov() + True sage: x = P.base_ring().gen() sage: F = E * matrix([[1], [x], [x**2]]) sage: F @@ -19930,7 +19932,8 @@ cdef class Matrix(Matrix1): constant matrix representing the same kernel elements:: sage: K, row_profile_bis = E.krylov_kernel_basis(M) - sage: row_profile == row_profile_bis + sage: row_profile == row_profile_bis + True sage: K, row_profile ( [82 76 0 40 0 1] @@ -19939,8 +19942,25 @@ cdef class Matrix(Matrix1): ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) ) - Observe that the output Krylov matrix row profile also allows one to - switch between the polynomial representation and the constant one:: + The computed matrix is a basis of the left kernel of the Krylov matrix + built with the appropriate degrees; note that the sum of these degrees + has to be `r = 3` and the Krylov matrix has `m+r = 6` rows:: + + sage: degrees = [max(rp[1] for rp in row_profile if rp[0] == j) + ....: for j in range(E.nrows())] + sage: degrees + [2, 1, 0] + sage: A = E.krylov_matrix(M, degrees=degrees) + sage: A.left_kernel_matrix(basis="pivot") + [96 96 1 0 0 0] + [13 57 0 3 1 0] + [82 76 0 40 0 1] + + The latter matrix and `K` coincide only up to row permutation, because + the order of the rows of `K` is inferred from the shifts, and makes it + echelon only up to row permutation. The row profile information also + allows one to directly relate the polynomial representation and the + constant one:: sage: all(P[:,j] == sum(K[:,k] * x**(row_profile[k][1]) ....: for k in range(K.ncols()) @@ -19948,85 +19968,102 @@ cdef class Matrix(Matrix1): ....: for j in range(P.ncols())) True - Other shifts will lead to different kernel bases:: + Since the shifts impact how the Krylov matrix is constructed, changing + shifts will lead to different kernel bases, yet representing the same + `\Bold{K}[x]`-module. The next shift yields a triangular polynomial + basis (the one in Hermite normal form):: sage: shifts = [0,3,6] - sage: E.krylov_kernel_basis(M, shifts=shifts, output_rows=False, var='x') + sage: H = E.krylov_kernel_basis(M, shifts=shifts, + ....: output_rows=False, var=x) + sage: H [ x^3 0 0] [60*x^2 + 72*x + 70 1 0] [60*x^2 + 72*x + 69 0 1] - sage: E.krylov_kernel_basis(M, shifts=shifts, output_rows=False) + sage: H.is_popov(shifts=shifts) and H.is_hermite(lower_echelon=True) + True + sage: P.popov_form(shifts=shifts) == H # same K[x]-module + True + + We again check the kernel basis property:: + + sage: K, row_profile = E.krylov_kernel_basis(M, shifts=shifts) + sage: K [ 0 0 0 1 0 0] [70 72 60 0 1 0] [69 72 60 0 0 1] + sage: degrees = [max(rp[1] for rp in row_profile if rp[0] == j) + ....: for j in range(E.nrows())] + sage: degrees + [3, 0, 0] + sage: A = E.krylov_matrix(M, shifts=shifts, degrees=degrees) + sage: K == A.left_kernel_matrix(basis="pivot") + True - sage: shifts = [3,0,2] - sage: E.krylov_kernel_basis(M, shifts=shifts, var='x') - ( - [ 1 26*x^2 + 49*x + 79 0] - [ 0 x^3 0] - [ 0 26*x^2 + 49*x + 78 1] - ((1, 0, 0), (1, 1, 1), (1, 2, 2), (2, 0, 3), (0, 0, 4), (1, 3, 5)) - ) - sage: E.krylov_kernel_basis(M, shifts=shifts) - ( - [79 49 26 0 1 0] - [ 0 0 0 0 0 1] - [78 49 26 1 0 0], - ((1, 0, 0), (1, 1, 1), (1, 2, 2), (2, 0, 3), (0, 0, 4), (1, 3, 5)) - ) - - TESTS:: + Here is another shift. Notice that the variable is allowed to be from a + multivariate ring:: - sage: R = GF(97) - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) - sage: basis, row_profile = E.krylov_kernel_basis(M, var='x') - sage: basis_coeff, row_profile_coeff = E.krylov_kernel_basis(M) - sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False) - sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] - True - sage: krylov_matrix = E.krylov_matrix(M,degrees=[basis[i,i].degree() for i in range(E.nrows())]) - sage: basis.is_popov() - True - sage: basis.degree() <= E.ncols() - True - sage: basis_coeff[0] * krylov_matrix == 0 - True - sage: len(E.krylov_basis(M)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) - True - sage: shifts = [0,3,6] - sage: basis = E.krylov_kernel_basis(M, shifts=shifts, output_rows=False, var='x') - sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) - sage: basis_coeff = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts) - sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) - sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] - True - sage: krylov_matrix = E.krylov_matrix(M,shifts=shifts,degrees=[basis[i,i].degree() for i in range(E.nrows())]) - sage: basis.is_popov(shifts=shifts) - True - sage: basis.degree() <= E.ncols() - True - sage: basis_coeff[0] * krylov_matrix == 0 - True - sage: len(E.krylov_basis(M, shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) - True + sage: bivring. = R[] sage: shifts = [3,0,2] - sage: basis = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts, var='x') - sage: perm = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts=shifts, degrees=[basis.degree()]*E.nrows())]) - sage: basis_coeff = E.krylov_kernel_basis(M, output_rows=False, shifts=shifts) - sage: basis_extended = matrix.block([[basis.coefficient_matrix(i) for i in range(basis.degree()+1)]],subdivide=False).with_permuted_columns(perm) - sage: basis_extended.matrix_from_columns([i for i in range(basis_extended.ncols()) if basis_extended[:,i] != 0]) == basis_coeff[0] - True - sage: krylov_matrix = E.krylov_matrix(M,shifts=shifts,degrees=[basis[i,i].degree() for i in range(E.nrows())]) - sage: basis.is_popov(shifts=shifts) - True - sage: basis.degree() <= E.ncols() + sage: Q, row_profile = E.krylov_kernel_basis(M, shifts=shifts, var=Y) + sage: Q + [ 1 26*Y^2 + 49*Y + 79 0] + [ 0 Y^3 0] + [ 0 26*Y^2 + 49*Y + 78 1] + sage: Q.base_ring() + Univariate Polynomial Ring in Y over Finite Field of size 97 + sage: Q.is_popov(shifts=shifts) True - sage: basis_coeff[0] * krylov_matrix == 0 + sage: P.popov_form(shifts=shifts) == Q(x) # same module True - sage: len(E.krylov_basis(M, shifts=shifts)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + + If we have a priori degree knowledge, we can use it to compute `K`:: + + sage: K, row_profile_bis = E.krylov_kernel_basis(M, shifts=shifts, + ....: degrees=[0, 3, 0], output_rows=True) + sage: K + [79 49 26 0 1 0] + [ 0 0 0 0 0 1] + [78 49 26 1 0 0] + sage: row_profile == row_profile_bis True + + sage: row_profile + ((1, 0, 0), (1, 1, 1), (1, 2, 2), (2, 0, 3), (0, 0, 4), (1, 3, 5)) + sage: degrees = [max(rp[1] for rp in row_profile if rp[0] == j) + ....: for j in range(E.nrows())] + sage: degrees + [0, 3, 0] + sage: A = E.krylov_matrix(M, shifts=shifts, degrees=degrees) + sage: A.left_kernel_matrix(basis="pivot") + [78 49 26 1 0 0] + [79 49 26 0 1 0] + [ 0 0 0 0 0 1] + + If one (or more) of the degree bounds it too low, the output matrix is + likely to be incorrect. In the next example, the bounds `3` are large + enough (since the minimal polynomial of `M` has degree at most `3`). + However we see from the above computation that `1` is strictly smaller + than the actual degree (which is 3) for the second row of `E`, when + using the given shifts. As a result, we get a different output, with a + sum of degrees which is less than `r=3` and therefore only `5` rows in + the associated Krylov matrix (which should have `m+r=6` rows), and with + a matrix which is not left-equivalent to the correct basis `K` above:: + + sage: K2, row_profile2 = E.krylov_kernel_basis(M, shifts=shifts, + ....: degrees=[3, 1, 3]) + sage: K2 + [ 1 0 96 1 0 0] + [ 3 28 56 0 1 0] + [47 64 69 0 0 1] + sage: 0 == K2 * E.krylov_matrix(M, shifts=shifts, degrees=[0, 3, 0]) + False + sage: degrees = [max(rp[1] for rp in row_profile2 if rp[0] == j) + ....: for j in range(E.nrows())] + sage: degrees + [0, 1, 1] + sage: E.krylov_matrix(M, shifts=shifts, degrees=degrees).nrows() + 5 """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix @@ -20053,7 +20090,7 @@ cdef class Matrix(Matrix1): except AttributeError: pass if not generator: - raise TypeError('polynomial variable must be a string or univariate polynomial ring generator, not {0}'.format(var)) + raise TypeError('polynomial variable must be a string or polynomial ring generator, not {0}'.format(var)) elif var.base_ring() != E.base_ring(): raise TypeError('polynomial generator must be over the same ring as the matrix entries') @@ -20076,27 +20113,35 @@ cdef class Matrix(Matrix1): m = E.nrows() sigma = E.ncols() base_ring = E.base_ring() - if var is None: - # convert c,d to actual position in striped Krylov matrix - phi = lambda row,deg : sum(min(max(shifts[row] - shifts[i] + deg + (i < row and shifts[i] <= shifts[row] + deg), 0), degrees[i] + 1) for i in range(m)) - # calculate krylov profile + # calculate krylov basis and rank profiles krylov_basis, row_profile = E.krylov_basis(M, shifts, degrees) col_profile = krylov_basis.pivots() + # method to convert c,d to actual position in Krylov matrix + phi = lambda row,deg : sum(min(max(shifts[row] - shifts[i] + deg + (i < row and shifts[i] <= shifts[row] + deg), 0), degrees[i] + 1) for i in range(m)) + + # deal with easy case: Krylov rank is zero + # -> polynomial kernel basis is the m x m identity + # -> constant kernel basis is the m x m identity column-permuted + # according to shifts if len(row_profile) == 0: - if var is None: - coefficients = matrix.identity(base_ring, m) + row_coords_coeff = E._krylov_row_coordinates(shifts, degrees, [(i, 0) for i in range(m)]) + row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) - row_coords_coeff = E._krylov_row_coordinates(shifts, degrees, [(i, 0) for i in range(m)]) + if var is None: + kkbasis = matrix.identity(base_ring, m) permutation = Permutation([x[2]+1 for x in row_coords_coeff]) - coefficients.permute_columns(permutation) - - row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) - - return coefficients, row_coords_krylov + kkbasis.permute_columns(permutation) + + else: + poly_ring = PolynomialRing(base_ring, var) + kkbasis = matrix.identity(poly_ring, m) + + if output_rows: + return kkbasis, row_coords_krylov else: - return matrix.identity(PolynomialRing(base_ring, var), m) + return kkbasis c, d, _ = zip(*(row for row in row_profile)) @@ -20137,11 +20182,6 @@ cdef class Matrix(Matrix1): kkbasis.permute_columns(permutation) else: - if isinstance(var, str): - poly_ring = PolynomialRing(base_ring, var) - else: - poly_ring = var.parent() - # construct coefficient map coeffs_map = [[{} for _ in range(m)] for _ in range(m)] # add identity part of kernel basis @@ -20154,6 +20194,7 @@ cdef class Matrix(Matrix1): # convert to matrix # TODO slow for extension fields (2025-08-30), see Issue 40667 + poly_ring = PolynomialRing(base_ring, var) kkbasis = matrix(poly_ring, m, m, coeffs_map) if output_rows: From 3cd8b9dfa0cac0edd352a764a441e731d1699a84 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 4 Sep 2025 12:08:46 +0200 Subject: [PATCH 49/57] add commas after spaces, add test cases for type checking, remove unnecessary imports --- src/sage/matrix/matrix2.pyx | 536 +++++++++++++++++++++++++++++------- 1 file changed, 429 insertions(+), 107 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 12fdb35b316..a7dbd58f83d 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19139,7 +19139,7 @@ cdef class Matrix(Matrix1): the maximum degree for rows. - ``row_pairs`` -- the list of pairs of row indices and associated degrees to be sorted. If ``None``, the default is taken as - `[(i,j), 0 \le i < m, 0 \le j \le d_i]` where `m` is + `[(i, j), 0 \le i < m, 0 \le j \le d_i]` where `m` is ``self.nrows()`` and `d_i` is ``degrees[i]``. OUTPUT: @@ -19152,9 +19152,9 @@ cdef class Matrix(Matrix1): EXAMPLES:: sage: R = GF(97) - sage: E = matrix.zero(R,3) - sage: shifts = vector(ZZ,[0,0,0]) - sage: degrees = vector(ZZ,[3,3,3]) + sage: E = matrix.zero(R, 3) + sage: shifts = vector(ZZ,[0, 0, 0]) + sage: degrees = vector(ZZ,[3, 3, 3]) sage: E._krylov_row_coordinates(shifts, degrees) [(0, 0, 0), (1, 0, 1), @@ -19168,9 +19168,9 @@ cdef class Matrix(Matrix1): (0, 3, 9), (1, 3, 10), (2, 3, 11)] - sage: shifts = vector(ZZ,[3,0,2]) - sage: degrees = vector(ZZ,[1,3,2]) - sage: E._krylov_row_coordinates(shifts,degrees) + sage: shifts = vector(ZZ, [3, 0, 2]) + sage: degrees = vector(ZZ, [1, 3, 2]) + sage: E._krylov_row_coordinates(shifts, degrees) [(1, 0, 1), (1, 1, 4), (1, 2, 6), @@ -19180,7 +19180,7 @@ cdef class Matrix(Matrix1): (2, 1, 5), (0, 1, 3), (2, 2, 7)] - sage: E._krylov_row_coordinates(shifts,degrees,[(2,2),(1,4),(1,2),(1,1),(0,2)]) + sage: E._krylov_row_coordinates(shifts,degrees, [(2, 2), (1, 4), (1, 2), (1, 1), (0, 2)]) [(1, 1, 2), (1, 2, 1), (2, 2, 0)] """ # (construct and) filter rows @@ -19201,17 +19201,17 @@ cdef class Matrix(Matrix1): multiplication matrix `M`. Here, ``self`` is some `m \times n` matrix `E` with rows denoted by `E_0, \ldots, E_{m-1}`, and `M` is a square `n \times n` matrix; ``degrees`` is a list of `m` nonnegative integers - `[d_0,\ldots,d_{m-1}]`. This Krylov matrix has `d_0+\cdots+d_{m-1}+m` + `[d_0, \ldots, d_{m-1}]`. This Krylov matrix has `d_0+\cdots+d_{m-1}+m` rows and `n` columns and is formed by stacking the Krylov iterates `E_{i} \cdot M^j` for all `0 \le i < m` and `0 \le j \le d_i`. These rows are ordered according to a priority defined by ``shifts``. - By default, ``shifts`` is taken as `[0,\ldots,0]`, and ``degrees`` is + By default, ``shifts`` is taken as `[0, \ldots, 0]`, and ``degrees`` is taken as `[n, \ldots, n]`. If a single integer `d` is provided for - ``degrees``, then it is interpreted as `[d,\ldots,d]`. + ``degrees``, then it is interpreted as `[d, \ldots, d]`. For example, for the default ``shifts`` equal to `[0, \ldots, 0]` and - for ``degrees`` equal to `[d,\ldots,d]`, the returned matrix is equal + for ``degrees`` equal to `[d, \ldots, d]`, the returned matrix is equal to .. MATH:: @@ -19223,9 +19223,9 @@ cdef class Matrix(Matrix1): E M^d \end{bmatrix} . - Another classical case is when ``shifts`` is `[0,k,2k,\ldots,(m-1)k]` - for a large `k` such as `k = d_0+\cdots+d_{m-1}`: then the Krylov - matrix is + Another classical case is when ``shifts`` is + `[0, k, 2k, \ldots, (m-1)k]` for a large `k` such as + `k = d_0+\cdots+d_{m-1}`: then the Krylov matrix is .. MATH:: @@ -19252,7 +19252,7 @@ cdef class Matrix(Matrix1): priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers (optional). The entry ``degrees[i]`` indicates the number of Krylov - iterates to appear in the output (that is, ``self[i,:] * M**j`` will + iterates to appear in the output (that is, ``self[i, :] * M**j`` will appear for `j` up to ``degrees[i]``, included). If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated @@ -19272,12 +19272,12 @@ cdef class Matrix(Matrix1): EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) sage: E [27 49 29] [50 58 0] [77 10 29] - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: M [0 1 0] [0 0 1] @@ -19298,7 +19298,7 @@ cdef class Matrix(Matrix1): Trying the other above-mentioned classical shift:: - sage: shifts = [0,3,6] + sage: shifts = [0, 3, 6] sage: E.krylov_matrix(M, shifts) [27 49 29] [ 0 27 49] @@ -19315,7 +19315,7 @@ cdef class Matrix(Matrix1): With another shift:: - sage: shifts = [3,0,2] + sage: shifts = [3, 0, 2] sage: E.krylov_matrix(M, shifts) [50 58 0] [ 0 50 58] @@ -19332,29 +19332,144 @@ cdef class Matrix(Matrix1): Specifying degree bounds via ``degrees``:: - sage: E.krylov_matrix(M, shifts, degrees=[0,2,1]) + sage: E.krylov_matrix(M, shifts, degrees=[0, 2, 1]) [50 58 0] [ 0 50 58] [ 0 0 50] [77 10 29] [27 49 29] [ 0 77 10] + + TESTS: + + The input `M` must be a matrix:: + + sage: R = GF(97) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: E.krylov_matrix(20) + Traceback (most recent call last): + ... + TypeError: M must be a matrix. + + The matrix `M` must be square and match `E.ncols()`:: + + sage: E.krylov_matrix(matrix.zero(R, 3, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_matrix(matrix.zero(R, 4, 3)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_matrix(matrix.zero(R, 4, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_matrix(matrix.zero(R, 3, 3)) + [27 49 29] + [50 58 0] + [77 10 29] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + + Base ring coercion should be possible in both directions:: + + sage: R2. = GF(97^2) + sage: M = matrix(R2, [[0, 45*z+67, 0], [0, 0, 20*z], [0, 0, 0]]) + sage: E.krylov_matrix(M) + [ 27 49 29] + [ 50 58 0] + [ 77 10 29] + [ 0 51*z + 63 10*z] + [ 0 19*z + 52 93*z] + [ 0 70*z + 18 6*z] + [ 0 0 49*z + 41] + [ 0 0 62*z + 40] + [ 0 0 14*z + 81] + [ 0 0 0] + [ 0 0 0] + [ 0 0 0] + sage: M.krylov_matrix(E) + [ 0 45*z + 67 0] + [ 0 0 20*z] + [ 0 0 0] + [19*z + 52 88*z + 6 0] + [ 85*z 6*z 95*z] + [ 0 0 0] + [63*z + 55 21*z + 83 66*z + 53] + [ 16*z 31*z 79*z] + [ 0 0 0] + [73*z + 16 18*z + 85 55*z + 28] + [ 14*z 74*z 39*z] + [ 0 0 0] + + But not possible if the base ring has a different characteristic:: + + sage: M = matrix(GF(11), [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) + sage: E.krylov_matrix(M) + Traceback (most recent call last): + ... + TypeError: no common canonical parent for objects with parents: 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 97' and 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 11' + + If shifts is a list, it must have the correct number of elements:: + + sage: E.krylov_matrix(M, shifts=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + + It must not contain any elements that cannot be converted to integers:: + + sage: E.krylov_matrix(M, shifts=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The same applies to degrees:: + + sage: E.krylov_matrix(M, degrees=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + sage: E.krylov_matrix(M, degrees=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The degree bounds must be non-negative:: + + sage: E.krylov_matrix(M, degrees=[2, 3, -1]) + Traceback (most recent call last) + ... + ValueError: degrees must not contain a negative bound. """ from sage.combinat.permutation import Permutation - import math - from sage.matrix.constructor import matrix from sage.modules.free_module_element import vector, FreeModuleElement E = self # INPUT VALIDATION if not isinstance(M, Matrix): - raise TypeError("krylov_matrix: M is not a matrix") + raise TypeError("M must be a matrix.") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("krylov_matrix: matrix M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions.") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") + if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) elif isinstance(degrees, (FreeModuleElement, list, tuple)): @@ -19362,14 +19477,7 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"krylov_matrix: degrees must not contain a negative bound.") - - if shifts is None: - shifts = (ZZ**E.nrows()).zero() - elif isinstance(shifts, (FreeModuleElement, list, tuple)): - shifts = (ZZ**E.nrows())(shifts) - else: - raise ValueError(f"krylov_matrix: shifts is not an integer vector of length {E.nrows()}.") + raise ValueError(f"degrees must not contain a negative bound.") m = E.nrows() iterations = max(degrees, default=ZZ.zero()).nbits() @@ -19378,7 +19486,7 @@ cdef class Matrix(Matrix1): blocks = [E] # keep track of indices - indices = [(i,0) for i in range(m)] + indices = [(i, 0) for i in range(m)] # for i from 0 to degree (inclusive), merge existing blocks, add E*M^i M_i = None @@ -19387,13 +19495,13 @@ cdef class Matrix(Matrix1): M_i = M else: M_i = M_i * M_i - blocks = [blocks[0].stack(blocks[1],subdivide=False)] + blocks = [blocks[0].stack(blocks[1], subdivide=False)] new_indices = [(row[0], row[1] + 2**i) for row in indices] - blocks.append((blocks[0].matrix_from_rows([i for i,x in enumerate(new_indices) if x[1] <= degrees[x[0]]]) * M_i)) + blocks.append((blocks[0].matrix_from_rows([i for i, x in enumerate(new_indices) if x[1] <= degrees[x[0]]]) * M_i)) indices.extend([x for x in new_indices if x[1] <= degrees[x[0]]]) # return block matrix permuted according to shifts - krylov = blocks[0].stack(blocks[1],subdivide=False) if len(blocks) > 1 else blocks[0] + krylov = blocks[0].stack(blocks[1], subdivide=False) if len(blocks) > 1 else blocks[0] permutation = Permutation([x[2] + 1 for x in E._krylov_row_coordinates(shifts, degrees)]) @@ -19407,8 +19515,8 @@ cdef class Matrix(Matrix1): TESTS:: sage: R = GF(97) - sage: E = matrix(R, [[27,49,29],[50,58,0],[77,10,29]]) - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: E._krylov_basis_naive(M, [0, 0, 0], [3, 3, 3], True) ( [27 49 29] @@ -19416,7 +19524,7 @@ cdef class Matrix(Matrix1): [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: shifts = [0,3,6] + sage: shifts = [0, 3, 6] sage: E._krylov_basis_naive(M, shifts, [3, 3, 3], True) ( [27 49 29] @@ -19424,7 +19532,7 @@ cdef class Matrix(Matrix1): [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) - sage: shifts = [3,0,2] + sage: shifts = [3, 0, 2] sage: E._krylov_basis_naive(M, shifts, [3, 3, 3], True) ( [50 58 0] @@ -19432,11 +19540,9 @@ cdef class Matrix(Matrix1): [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) ) """ - from sage.matrix.constructor import matrix - m = self.nrows() - K = self.krylov_matrix(M,shifts,degrees) + K = self.krylov_matrix(M, shifts, degrees) # calculate first independent rows of K row_profile = K.pivot_rows() @@ -19461,8 +19567,8 @@ cdef class Matrix(Matrix1): TESTS:: sage: R = GF(97) - sage: E = matrix(R, [[27,49,29],[50,58,0],[77,10,29]]) - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: E._krylov_basis_elimination(M, [0, 0, 0], [3, 3, 3], True) ( [27 49 29] @@ -19470,7 +19576,7 @@ cdef class Matrix(Matrix1): [ 0 27 49], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) ) - sage: shifts = [0,3,6] + sage: shifts = [0, 3, 6] sage: E._krylov_basis_elimination(M, shifts, [3, 3, 3], True) ( [27 49 29] @@ -19478,7 +19584,7 @@ cdef class Matrix(Matrix1): [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) - sage: shifts = [3,0,2] + sage: shifts = [3, 0, 2] sage: E._krylov_basis_elimination(M, shifts, [3, 3, 3], True) ( [50 58 0] @@ -19487,7 +19593,6 @@ cdef class Matrix(Matrix1): ) """ from sage.matrix.constructor import matrix - import math from sage.combinat.permutation import Permutation m = self.nrows() @@ -19498,7 +19603,7 @@ cdef class Matrix(Matrix1): return (self, ()) if output_rows else self # calculate row profile of self, with shifts applied - self_rows = [x[2] for x in self._krylov_row_coordinates(shifts, degrees, [(i,0) for i in range(m)])] + self_rows = [x[2] for x in self._krylov_row_coordinates(shifts, degrees, [(i, 0) for i in range(m)])] self_permutation = Permutation([x + 1 for x in self_rows]) self_permuted = self.with_permuted_rows(self_permutation) @@ -19540,7 +19645,7 @@ cdef class Matrix(Matrix1): rows = [i for i, x in enumerate(row_profile_self) if x[1] + L <= degrees[x[0]]] S = (R*M_L).matrix_from_rows(rows) - R = matrix.block([[exhausted],[R],[S]], subdivide=False) + R = matrix.block([[exhausted], [R], [S]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) R.permute_rows(k_perm) @@ -19549,7 +19654,7 @@ cdef class Matrix(Matrix1): r = len(row_profile_R) if r == sigma and l < iterations - 1: - tail = list(range(row_profile_R[-1]+1,R.nrows())) + tail = list(range(row_profile_R[-1]+1, R.nrows())) excluded_rows.update(set([k[k_rows[i]][0] for i in tail if i < len(k)])) xmi = [i for i in row_profile_R if k[k_rows[i]][0] in excluded_rows] @@ -19583,8 +19688,8 @@ cdef class Matrix(Matrix1): if not output_rows: return R - # convert c,d to actual position in Krylov matrix - phi = lambda c,d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degrees[i] + 1) for i in range(m)) + # convert c, d to actual position in Krylov matrix + phi = lambda c, d : sum(min(max(shifts[c] - shifts[i] + d + (i < c and shifts[i] <= shifts[c] + d), 0), degrees[i] + 1) for i in range(m)) row_profile = tuple([(*row, phi(*row)) for row in row_profile_self]) return R, row_profile @@ -19599,7 +19704,7 @@ cdef class Matrix(Matrix1): Krylov subspace of `E` and `M`, that is, the span of all the above row vectors. - A list ``degrees`` of integers `[d_0,\ldots,d_{m-1}]` can be provided + A list ``degrees`` of integers `[d_0, \ldots, d_{m-1}]` can be provided to indicate known maximal exponents, such that `E_{k} \cdot M^{d_k-1}` may appear in the output basis, but `E_{k} \cdot M^{d_k}` will not. In other words, `E_{k} \cdot M^{d_k}` is known to @@ -19629,7 +19734,7 @@ cdef class Matrix(Matrix1): - ``shifts`` -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers - (optional). The entry ``degrees[i]`` indicates that ``self[i,:] * + (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for @@ -19662,10 +19767,10 @@ cdef class Matrix(Matrix1): EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: K = E.krylov_matrix(M) - sage: K[:6,:] + sage: K[:6, :] [27 49 29] [50 58 0] [77 10 29] @@ -19691,7 +19796,7 @@ cdef class Matrix(Matrix1): rows in the Krylov matrix `K`:: sage: degrees = 3 # default == self.ncols() - sage: rows = [(i,j,i+j*E.nrows()) for j in range(degrees+1) + sage: rows = [(i, j, i+j*E.nrows()) for j in range(degrees+1) ....: for i in range(E.nrows())] sage: rows[:6] [(0, 0, 0), @@ -19704,8 +19809,8 @@ cdef class Matrix(Matrix1): Now with another shift, the first three rows of the Krylov matrix are independent and thus form the Krylov basis:: - sage: shifts = [0,3,6] - sage: E.krylov_matrix(M, shifts=shifts)[:3,:] + sage: shifts = [0, 3, 6] + sage: E.krylov_matrix(M, shifts=shifts)[:3, :] [27 49 29] [ 0 27 49] [ 0 0 27] @@ -19716,7 +19821,7 @@ cdef class Matrix(Matrix1): [ 0 0 27], ((0, 0, 0), (0, 1, 1), (0, 2, 2)) ) sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) - sage: [(i,j) for (i,j,k) in rows[:3]] + sage: [(i, j) for (i, j, k) in rows[:3]] [(0, 0), (0, 1), (0, 2)] @@ -19724,8 +19829,8 @@ cdef class Matrix(Matrix1): Another shift for which the first three rows of the Krylov matrix are independent:: - sage: shifts = [3,0,2] - sage: E.krylov_matrix(M, shifts=shifts)[:6,:] + sage: shifts = [3, 0, 2] + sage: E.krylov_matrix(M, shifts=shifts)[:6, :] [50 58 0] [ 0 50 58] [ 0 0 50] @@ -19739,10 +19844,114 @@ cdef class Matrix(Matrix1): [ 0 0 50], ((1, 0, 0), (1, 1, 1), (1, 2, 2)) ) sage: rows.sort(key=lambda x: (x[1] + shifts[x[0]], x[0])) - sage: [(i,j) for (i,j,k) in rows[:3]] + sage: [(i, j) for (i, j, k) in rows[:3]] [(1, 0), (1, 1), (1, 2)] + + TESTS: + + The base ring of `self` must be an exact field:: + + sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) + sage: E.krylov_basis(E) + Traceback (most recent call last) + ... + NotImplementedError: self.base_ring() must be an exact field + + The input `M` must be a matrix:: + + sage: R = GF(97) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: E.krylov_basis(20) + Traceback (most recent call last): + ... + TypeError: M must be a matrix. + + The matrix `M` must be square and match `E.ncols()`:: + + sage: E.krylov_basis(matrix.zero(R, 3, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_basis(matrix.zero(R, 4, 3)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_basis(matrix.zero(R, 4, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_basis(matrix.zero(R, 3, 3)) + ( + [27 49 29] + [50 58 0], ((0, 0, 0), (1, 0, 1)) + ) + + Base ring coercion should be possible in both directions:: + + sage: R2. = GF(97^2) + sage: M = matrix(R2, [[0, 45*z+67, 0], [0, 0, 20*z], [0, 0, 0]]) + sage: E.krylov_basis(M) + ( + [ 27 49 29] + [ 50 58 0] + [ 0 51*z + 63 10*z], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) + ) + sage: M.krylov_basis(E) + ( + [ 0 45*z + 67 0] + [ 0 0 20*z] + [19*z + 52 88*z + 6 0], ((0, 0, 0), (1, 0, 1), (0, 1, 3)) + ) + + But not possible if the base ring has a different characteristic:: + + sage: M = matrix(GF(11), [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) + sage: E.krylov_basis(M) + Traceback (most recent call last): + ... + TypeError: no common canonical parent for objects with parents: 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 97' and 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 11' + + If shifts is a list, it must have the correct number of elements:: + + sage: E.krylov_basis(M, shifts=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + + It must not contain any elements that cannot be converted to integers:: + + sage: E.krylov_basis(M, shifts=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The same applies to degrees:: + + sage: E.krylov_basis(M, degrees=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + sage: E.krylov_basis(M, degrees=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The degree bounds must be non-negative:: + + sage: E.krylov_basis(M, degrees=[2, 3, -1]) + Traceback (most recent call last) + ... + ValueError: degrees must not contain a negative bound. + + The algorithm must be valid:: + + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: E.krylov_basis(M, algorithm="non-existent") + Traceback (most recent call last): + ... + ValueError: algorithm must be one of None, "naive" or "elimination". """ from sage.modules.free_module_element import vector @@ -19750,15 +19959,22 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise NotImplementedError("krylov_basis: matrix entries must come from an exact field") + raise NotImplementedError("self.base_ring() must be an exact field.") if not isinstance(M, Matrix): - raise TypeError("krylov_basis: M is not a matrix") + raise TypeError("M must be a matrix.") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("krylov_basis: matrix M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions.") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") + if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) elif isinstance(degrees, (FreeModuleElement, list, tuple)): @@ -19766,14 +19982,7 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"krylov_basis: degrees must not contain a negative bound.") - - if shifts is None: - shifts = (ZZ**E.nrows()).zero() - elif isinstance(shifts, (FreeModuleElement, list, tuple)): - shifts = (ZZ**E.nrows())(shifts) - else: - raise ValueError(f"krylov_basis: shifts is not an integer vector of length {E.nrows()}.") + raise ValueError(f"degrees must not contain a negative bound.") if algorithm is None: if E.base_ring().order() == 2: @@ -19798,7 +20007,7 @@ cdef class Matrix(Matrix1): else: raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") - def krylov_kernel_basis(self, M, shifts=None, degrees=None, output_rows=True, var=None): + def krylov_kernel_basis(self, M, shifts=None, degrees=None, output_rows=True, var=None, basis_algorithm=None): r""" Return a basis in canonical form for the kernel of the Krylov matrix of ``(self, M)`` with rows ordered according to ``shifts``. In other terms, @@ -19808,7 +20017,7 @@ cdef class Matrix(Matrix1): Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters - `M`, ``shifts``, and ``degrees``. Let `[\delta_0,\ldots,\delta_{m-1}]` + `M`, ``shifts``, and ``degrees``. Let `[\delta_0, \ldots, \delta_{m-1}]` be the exponents of first linear dependency for each row. That is, if the `i`-th row `E_i` of `E` has not been selected for appearing in `B` then `\delta_i = 0`, and otherwise, `\delta_i` is such that `E_i \cdot @@ -19819,7 +20028,7 @@ cdef class Matrix(Matrix1): The returned matrix `K` is a basis, in reduced row echelon form (upper echelon, and up to row permutation), of the left kernel of the Krylov matrix `A` built from `E` and `M` with ``shifts`` and ``degrees`` equal - to `[\delta_0,\ldots,\delta_{m-1}]`. Recall from :meth:`krylov_matrix` + to `[\delta_0, \ldots, \delta_{m-1}]`. Recall from :meth:`krylov_matrix` that this matrix `A` is, up to a row permutation deduced from ``shifts``, equal to @@ -19849,7 +20058,7 @@ cdef class Matrix(Matrix1): from the matrix `K` one can deduce for free (up to copying rows and shifting coefficients to the right) left kernel bases for Krylov matrices that would be built from the same `E`, `M`, and ``shifts``, - but with larger degrees, that is `[d_0,\ldots,d_{m-1}]` with `d_i \ge + but with larger degrees, that is `[d_0, \ldots, d_{m-1}]` with `d_i \ge \delta_i` for all `i`. .. WARNING:: @@ -19869,7 +20078,7 @@ cdef class Matrix(Matrix1): - ``shifts`` -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - ``degrees`` -- an integer or a list of ``self.nrows()`` integers - (optional). The entry ``degrees[i]`` indicates that ``self[i,:] * + (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for @@ -19883,6 +20092,8 @@ cdef class Matrix(Matrix1): representation is used, and ``var`` must be either the generator of a polynomial ring over the base ring of ``self``, or a string which will be used as variable name for building such a polynomial ring. + - ``algorithm`` -- either 'naive', 'elimination', or None (let + Sage choose). OUTPUT: @@ -19906,8 +20117,8 @@ cdef class Matrix(Matrix1): EXAMPLES:: sage: R = GF(97) - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) - sage: M = matrix(R, [[0,1,0], [0,0,1], [0,0,0]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) This example corresponds to computing a basis of Hermite-Padé approximants:: @@ -19962,7 +20173,7 @@ cdef class Matrix(Matrix1): allows one to directly relate the polynomial representation and the constant one:: - sage: all(P[:,j] == sum(K[:,k] * x**(row_profile[k][1]) + sage: all(P[:, j] == sum(K[:, k] * x**(row_profile[k][1]) ....: for k in range(K.ncols()) ....: if row_profile[k][0] == j) ....: for j in range(P.ncols())) @@ -19973,7 +20184,7 @@ cdef class Matrix(Matrix1): `\Bold{K}[x]`-module. The next shift yields a triangular polynomial basis (the one in Hermite normal form):: - sage: shifts = [0,3,6] + sage: shifts = [0, 3, 6] sage: H = E.krylov_kernel_basis(M, shifts=shifts, ....: output_rows=False, var=x) sage: H @@ -20003,8 +20214,8 @@ cdef class Matrix(Matrix1): Here is another shift. Notice that the variable is allowed to be from a multivariate ring:: - sage: bivring. = R[] - sage: shifts = [3,0,2] + sage: bivring. = R[] + sage: shifts = [3, 0, 2] sage: Q, row_profile = E.krylov_kernel_basis(M, shifts=shifts, var=Y) sage: Q [ 1 26*Y^2 + 49*Y + 79 0] @@ -20064,6 +20275,117 @@ cdef class Matrix(Matrix1): [0, 1, 1] sage: E.krylov_matrix(M, shifts=shifts, degrees=degrees).nrows() 5 + + TESTS: + + The base ring of `self` must be an exact field:: + + sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) + sage: E.krylov_kernel_basis(E) + Traceback (most recent call last) + ... + NotImplementedError: self.base_ring() must be an exact field + + The input `M` must be a matrix:: + + sage: R = GF(97) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) + sage: E.krylov_kernel_basis(20) + Traceback (most recent call last): + ... + TypeError: M must be a matrix. + + The matrix `M` must be square and match `E.ncols()`:: + + sage: E.krylov_kernel_basis(matrix.zero(R, 3, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_kernel_basis(matrix.zero(R, 4, 3)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_kernel_basis(matrix.zero(R, 4, 4)) + Traceback (most recent call last): + ... + ValueError: M does not have correct dimensions. + sage: E.krylov_kernel_basis(matrix.zero(R, 3, 3)) + ( + [ 0 0 0 1 0] + [ 0 0 0 0 1] + [96 96 1 0 0], + + ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4)) + ) + + Base ring coercion should be possible in both directions:: + + sage: R2. = GF(97^2) + sage: M = matrix(R2, [[0, 45*z+67, 0], [0, 0, 20*z], [0, 0, 0]]) + sage: E.krylov_kernel_basis(M) + ( + [19*z + 21 46*z + 10 0 13*z + 24 0 1] + [60*z + 66 84*z + 73 0 15*z + 83 1 0] + [ 96 96 1 0 0 0], + + ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) + ) + sage: M.krylov_kernel_basis(E) + ( + [ 86 50*z + 34 0 12 0 1] + [18*z + 59 68 0 29*z + 25 1 0] + [ 0 0 1 0 0 0], + + ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) + ) + + But not possible if the base ring has a different characteristic:: + + sage: M = matrix(GF(11), [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) + sage: E.krylov_kernel_basis(M) + Traceback (most recent call last): + ... + TypeError: no common canonical parent for objects with parents: 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 97' and 'Full MatrixSpace of 3 by 3 dense matrices over Finite Field of size 11' + + If shifts is a list, it must have the correct number of elements:: + + sage: E.krylov_kernel_basis(M, shifts=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + + It must not contain any elements that cannot be converted to integers:: + + sage: E.krylov_kernel_basis(M, shifts=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The same applies to degrees:: + + sage: E.krylov_kernel_basis(M, degrees=[2, 3]) + Traceback (most recent call last): + ... + TypeError: x must be a list of the right length + sage: E.krylov_kernel_basis(M, degrees=[2, 3, 4.5]) + Traceback (most recent call last): + ... + TypeError: Attempt to coerce non-integral RealNumber to Integer + + The degree bounds must be non-negative:: + + sage: E.krylov_kernel_basis(M, degrees=[2, 3, -1]) + Traceback (most recent call last) + ... + ValueError: degrees must not contain a negative bound. + + The algorithm must be valid:: + + sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: E.krylov_kernel_basis(M, base_algorithm="non-existent") + Traceback (most recent call last): + ... + ValueError: algorithm must be one of None, "naive" or "elimination". """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix @@ -20074,12 +20396,12 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise NotImplementedError("krylov_kernel_basis: matrix entries must come from an exact field") + raise NotImplementedError("self.base_ring() must be an exact field.") if not isinstance(M, Matrix): - raise TypeError("krylov_kernel_basis: M is not a matrix") + raise TypeError("M must be a matrix.") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("krylov_kernel_basis: matrix M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions.") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) @@ -20090,9 +20412,16 @@ cdef class Matrix(Matrix1): except AttributeError: pass if not generator: - raise TypeError('polynomial variable must be a string or polynomial ring generator, not {0}'.format(var)) + raise TypeError('polynomial variable must be a string or polynomial ring generator, not {0}.'.format(var)) elif var.base_ring() != E.base_ring(): - raise TypeError('polynomial generator must be over the same ring as the matrix entries') + raise TypeError('polynomial generator must be over the same ring as the matrix entries.') + + if shifts is None: + shifts = (ZZ**E.nrows()).zero() + elif isinstance(shifts, (FreeModuleElement, list, tuple)): + shifts = (ZZ**E.nrows())(shifts) + else: + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) @@ -20101,25 +20430,18 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"krylov_kernel_basis: degrees must not contain a negative bound.") - - if shifts is None: - shifts = (ZZ**E.nrows()).zero() - elif isinstance(shifts, (FreeModuleElement, list, tuple)): - shifts = (ZZ**E.nrows())(shifts) - else: - raise ValueError(f"krylov_kernel_basis: shifts is not an integer vector of length {E.nrows()}.") + raise ValueError(f"degrees must not contain a negative bound.") m = E.nrows() sigma = E.ncols() base_ring = E.base_ring() # calculate krylov basis and rank profiles - krylov_basis, row_profile = E.krylov_basis(M, shifts, degrees) + krylov_basis, row_profile = E.krylov_basis(M, shifts, degrees, algorithm=basis_algorithm) col_profile = krylov_basis.pivots() - # method to convert c,d to actual position in Krylov matrix - phi = lambda row,deg : sum(min(max(shifts[row] - shifts[i] + deg + (i < row and shifts[i] <= shifts[row] + deg), 0), degrees[i] + 1) for i in range(m)) + # method to convert c, d to actual position in Krylov matrix + phi = lambda row, deg : sum(min(max(shifts[row] - shifts[i] + deg + (i < row and shifts[i] <= shifts[row] + deg), 0), degrees[i] + 1) for i in range(m)) # deal with easy case: Krylov rank is zero # -> polynomial kernel basis is the m x m identity @@ -20173,11 +20495,11 @@ cdef class Matrix(Matrix1): relation = D*C.inverse() rows = list(zip(c, d)) + list(enumerate(degree_c)) - row_coords_coeff = E._krylov_row_coordinates(shifts,degrees,rows) + row_coords_coeff = E._krylov_row_coordinates(shifts, degrees, rows) row_coords_krylov = tuple([(*x[:2], phi(*x[:2])) for x in row_coords_coeff]) if var is None: - kkbasis = (-relation).augment(matrix.identity(base_ring,m)) + kkbasis = (-relation).augment(matrix.identity(base_ring, m)) permutation = Permutation([x[2]+1 for x in row_coords_coeff]) kkbasis.permute_columns(permutation) @@ -20190,7 +20512,7 @@ cdef class Matrix(Matrix1): # add relation part of kernel basis for col in range(relation.ncols()): for row in range(m): - coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], base_ring.zero()) - relation[row,col] + coeffs_map[row][c[col]][d[col]] = coeffs_map[row][c[col]].get(d[col], base_ring.zero()) - relation[row, col] # convert to matrix # TODO slow for extension fields (2025-08-30), see Issue 40667 From 5f8d45f8d1cc387296906802e2b440bfe074bd36 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 4 Sep 2025 12:11:18 +0200 Subject: [PATCH 50/57] add spaces after commas --- src/sage/matrix/matrix2.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index a7dbd58f83d..706fd6f4035 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19153,8 +19153,8 @@ cdef class Matrix(Matrix1): sage: R = GF(97) sage: E = matrix.zero(R, 3) - sage: shifts = vector(ZZ,[0, 0, 0]) - sage: degrees = vector(ZZ,[3, 3, 3]) + sage: shifts = vector(ZZ, [0, 0, 0]) + sage: degrees = vector(ZZ, [3, 3, 3]) sage: E._krylov_row_coordinates(shifts, degrees) [(0, 0, 0), (1, 0, 1), @@ -19180,7 +19180,7 @@ cdef class Matrix(Matrix1): (2, 1, 5), (0, 1, 3), (2, 2, 7)] - sage: E._krylov_row_coordinates(shifts,degrees, [(2, 2), (1, 4), (1, 2), (1, 1), (0, 2)]) + sage: E._krylov_row_coordinates(shifts, degrees, [(2, 2), (1, 4), (1, 2), (1, 1), (0, 2)]) [(1, 1, 2), (1, 2, 1), (2, 2, 0)] """ # (construct and) filter rows @@ -19947,7 +19947,7 @@ cdef class Matrix(Matrix1): The algorithm must be valid:: - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) sage: E.krylov_basis(M, algorithm="non-existent") Traceback (most recent call last): ... @@ -20381,7 +20381,7 @@ cdef class Matrix(Matrix1): The algorithm must be valid:: - sage: E = matrix(R, [[27,49,29], [50,58,0], [77,10,29]]) + sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) sage: E.krylov_kernel_basis(M, base_algorithm="non-existent") Traceback (most recent call last): ... From d540cc3611c469781f314d2a619df773ea1cd976 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 4 Sep 2025 15:08:15 +0200 Subject: [PATCH 51/57] fix doc tests --- src/sage/matrix/matrix2.pyx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 706fd6f4035..acb31757825 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19420,6 +19420,7 @@ cdef class Matrix(Matrix1): If shifts is a list, it must have the correct number of elements:: + sage: M = matrix(R, [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: E.krylov_matrix(M, shifts=[2, 3]) Traceback (most recent call last): ... @@ -19446,7 +19447,7 @@ cdef class Matrix(Matrix1): The degree bounds must be non-negative:: sage: E.krylov_matrix(M, degrees=[2, 3, -1]) - Traceback (most recent call last) + Traceback (most recent call last): ... ValueError: degrees must not contain a negative bound. """ @@ -19855,9 +19856,9 @@ cdef class Matrix(Matrix1): sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) sage: E.krylov_basis(E) - Traceback (most recent call last) + Traceback (most recent call last): ... - NotImplementedError: self.base_ring() must be an exact field + NotImplementedError: self.base_ring() must be an exact field. The input `M` must be a matrix:: @@ -19915,6 +19916,7 @@ cdef class Matrix(Matrix1): If shifts is a list, it must have the correct number of elements:: + sage: M = matrix(GF(97), [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: E.krylov_basis(M, shifts=[2, 3]) Traceback (most recent call last): ... @@ -19941,7 +19943,7 @@ cdef class Matrix(Matrix1): The degree bounds must be non-negative:: sage: E.krylov_basis(M, degrees=[2, 3, -1]) - Traceback (most recent call last) + Traceback (most recent call last): ... ValueError: degrees must not contain a negative bound. @@ -20282,9 +20284,9 @@ cdef class Matrix(Matrix1): sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) sage: E.krylov_kernel_basis(E) - Traceback (most recent call last) + Traceback (most recent call last): ... - NotImplementedError: self.base_ring() must be an exact field + NotImplementedError: self.base_ring() must be an exact field. The input `M` must be a matrix:: @@ -20314,7 +20316,6 @@ cdef class Matrix(Matrix1): [ 0 0 0 1 0] [ 0 0 0 0 1] [96 96 1 0 0], - ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4)) ) @@ -20327,7 +20328,6 @@ cdef class Matrix(Matrix1): [19*z + 21 46*z + 10 0 13*z + 24 0 1] [60*z + 66 84*z + 73 0 15*z + 83 1 0] [ 96 96 1 0 0 0], - ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) ) sage: M.krylov_kernel_basis(E) @@ -20335,7 +20335,6 @@ cdef class Matrix(Matrix1): [ 86 50*z + 34 0 12 0 1] [18*z + 59 68 0 29*z + 25 1 0] [ 0 0 1 0 0 0], - ((0, 0, 0), (1, 0, 1), (2, 0, 2), (0, 1, 3), (1, 1, 4), (0, 2, 6)) ) @@ -20349,6 +20348,7 @@ cdef class Matrix(Matrix1): If shifts is a list, it must have the correct number of elements:: + sage: M = matrix(GF(97), [[0, 1, 0], [0, 0, 1], [0, 0, 0]]) sage: E.krylov_kernel_basis(M, shifts=[2, 3]) Traceback (most recent call last): ... @@ -20375,14 +20375,14 @@ cdef class Matrix(Matrix1): The degree bounds must be non-negative:: sage: E.krylov_kernel_basis(M, degrees=[2, 3, -1]) - Traceback (most recent call last) + Traceback (most recent call last): ... ValueError: degrees must not contain a negative bound. The algorithm must be valid:: sage: E = matrix(R, [[27, 49, 29], [50, 58, 0], [77, 10, 29]]) - sage: E.krylov_kernel_basis(M, base_algorithm="non-existent") + sage: E.krylov_kernel_basis(M, basis_algorithm="non-existent") Traceback (most recent call last): ... ValueError: algorithm must be one of None, "naive" or "elimination". From 6582adedd31ad49790e57b2356399f8ef0a8d913 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Thu, 4 Sep 2025 17:01:57 +0200 Subject: [PATCH 52/57] review changes --- src/sage/matrix/matrix2.pyx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index acb31757825..3630060c23e 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19137,7 +19137,7 @@ cdef class Matrix(Matrix1): the priority given to rows. - ``degrees`` -- an integer vector of length ``self.nrows()`` defining the maximum degree for rows. - - ``row_pairs`` -- the list of pairs of row indices and associated + - ``row_pairs`` (default: ``None``) -- the list of pairs of row indices and associated degrees to be sorted. If ``None``, the default is taken as `[(i, j), 0 \le i < m, 0 \le j \le d_i]` where `m` is ``self.nrows()`` and `d_i` is ``degrees[i]``. @@ -19248,9 +19248,9 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` -- list of ``self.nrows()`` integers (optional), row + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional), row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers (optional). The entry ``degrees[i]`` indicates the number of Krylov iterates to appear in the output (that is, ``self[i, :] * M**j`` will appear for `j` up to ``degrees[i]``, included). If ``None``, defaults @@ -19732,19 +19732,19 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` -- list of ``self.nrows()`` integers (optional): row + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``output_rows`` -- boolean, optional, defaults to True. Determines + - ``output_rows`` (default: ``True``) -- boolean, optional. Determines whether information relating the output rows to their position in the Krylov matrix is also provided. - - ``algorithm`` -- either 'naive', 'elimination', or None (let + - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let Sage choose). OUTPUT: @@ -19987,12 +19987,12 @@ cdef class Matrix(Matrix1): raise ValueError(f"degrees must not contain a negative bound.") if algorithm is None: - if E.base_ring().order() == 2: + if E.base_ring().cardinality() == 2: if E.nrows() <= 12: algorithm = 'naive' else: algorithm = 'elimination' - elif E.base_ring().degree() == 1: + elif E.base_ring().is_prime_field(): if E.nrows() <= 4: algorithm = 'naive' else: @@ -20014,8 +20014,8 @@ cdef class Matrix(Matrix1): Return a basis in canonical form for the kernel of the Krylov matrix of ``(self, M)`` with rows ordered according to ``shifts``. In other terms, the rows of the returned matrix form a basis of the kernel of the - `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to K^n` given by the matrix - ``self``, where the action of `x` on `K^n` is given by `M`. + `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to \Bold{K}^n` given by the matrix + ``self``, where the action of `x` on `\Bold{K}^n` is given by `M`. Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters @@ -20077,24 +20077,24 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` -- list of ``self.nrows()`` integers (optional): row + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional): row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` -- an integer or a list of ``self.nrows()`` integers + - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``output_rows`` -- boolean, optional, defaults to True. Determines + - ``output_rows`` (default: ``True``) -- boolean, optional. Determines whether information relating the output columns to the rows of the corresponding Krylov matrix is also provided. - - ``var`` -- (optional) Defaults to ``None``, which means returning a + - ``var`` (default: ``None``) -- (optional) If ``None``, this method returns a constant matrix. If not ``None``, the polynomial matrix representation is used, and ``var`` must be either the generator of a polynomial ring over the base ring of ``self``, or a string which will be used as variable name for building such a polynomial ring. - - ``algorithm`` -- either 'naive', 'elimination', or None (let + - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let Sage choose). OUTPUT: @@ -20253,7 +20253,7 @@ cdef class Matrix(Matrix1): [79 49 26 0 1 0] [ 0 0 0 0 0 1] - If one (or more) of the degree bounds it too low, the output matrix is + If one (or more) of the degree bounds is too low, the output matrix is likely to be incorrect. In the next example, the bounds `3` are large enough (since the minimal polynomial of `M` has degree at most `3`). However we see from the above computation that `1` is strictly smaller From 7277781a57f3dd6d2807f521f2ab359db8498331 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 5 Sep 2025 10:04:04 +0200 Subject: [PATCH 53/57] review change --- src/sage/matrix/matrix2.pyx | 112 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index 3630060c23e..ba2c23dd3c4 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19248,10 +19248,9 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional), row + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers, row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers - (optional). The entry ``degrees[i]`` indicates the number of Krylov + - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers. The entry ``degrees[i]`` indicates the number of Krylov iterates to appear in the output (that is, ``self[i, :] * M**j`` will appear for `j` up to ``degrees[i]``, included). If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for @@ -19349,22 +19348,22 @@ cdef class Matrix(Matrix1): sage: E.krylov_matrix(20) Traceback (most recent call last): ... - TypeError: M must be a matrix. + TypeError: M must be a matrix The matrix `M` must be square and match `E.ncols()`:: sage: E.krylov_matrix(matrix.zero(R, 3, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_matrix(matrix.zero(R, 4, 3)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_matrix(matrix.zero(R, 4, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_matrix(matrix.zero(R, 3, 3)) [27 49 29] [50 58 0] @@ -19449,7 +19448,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_matrix(M, degrees=[2, 3, -1]) Traceback (most recent call last): ... - ValueError: degrees must not contain a negative bound. + ValueError: degrees must not contain a negative bound """ from sage.combinat.permutation import Permutation from sage.modules.free_module_element import vector, FreeModuleElement @@ -19458,9 +19457,9 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not isinstance(M, Matrix): - raise TypeError("M must be a matrix.") + raise TypeError("M must be a matrix") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) @@ -19469,7 +19468,7 @@ cdef class Matrix(Matrix1): elif isinstance(shifts, (FreeModuleElement, list, tuple)): shifts = (ZZ**E.nrows())(shifts) else: - raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}") if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) @@ -19478,7 +19477,7 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"degrees must not contain a negative bound.") + raise ValueError(f"degrees must not contain a negative bound") m = E.nrows() iterations = max(degrees, default=ZZ.zero()).nbits() @@ -19645,7 +19644,7 @@ cdef class Matrix(Matrix1): M_L = M_L * M_L rows = [i for i, x in enumerate(row_profile_self) if x[1] + L <= degrees[x[0]]] - S = (R*M_L).matrix_from_rows(rows) + S = R.matrix_from_rows(rows)*M_L R = matrix.block([[exhausted], [R], [S]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) @@ -19732,16 +19731,15 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional): row + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers: row priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers - (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * + - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers. The entry ``degrees[i]`` indicates that ``self[i, :] * M**degrees[i]`` is known to be dependent on rows before it in the ``shifts``-ordered Krylov matrix. If ``None``, defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``output_rows`` (default: ``True``) -- boolean, optional. Determines + - ``output_rows`` (default: ``True``) -- boolean. Determines whether information relating the output rows to their position in the Krylov matrix is also provided. - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let @@ -19852,13 +19850,13 @@ cdef class Matrix(Matrix1): TESTS: - The base ring of `self` must be an exact field:: + The method is currently only implemented over exact fields:: sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) sage: E.krylov_basis(E) Traceback (most recent call last): ... - NotImplementedError: self.base_ring() must be an exact field. + NotImplementedError: computing bases of Krylov matrices is currently only implemented over exact fields The input `M` must be a matrix:: @@ -19867,7 +19865,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_basis(20) Traceback (most recent call last): ... - TypeError: M must be a matrix. + TypeError: M must be a matrix The matrix `M` must be square and match `E.ncols()`:: @@ -19945,7 +19943,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_basis(M, degrees=[2, 3, -1]) Traceback (most recent call last): ... - ValueError: degrees must not contain a negative bound. + ValueError: degrees must not contain a negative bound The algorithm must be valid:: @@ -19953,7 +19951,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_basis(M, algorithm="non-existent") Traceback (most recent call last): ... - ValueError: algorithm must be one of None, "naive" or "elimination". + ValueError: algorithm must be one of None, "naive" or "elimination" """ from sage.modules.free_module_element import vector @@ -19961,12 +19959,12 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise NotImplementedError("self.base_ring() must be an exact field.") + raise NotImplementedError("computing bases of Krylov matrices is currently only implemented over exact fields") if not isinstance(M, Matrix): - raise TypeError("M must be a matrix.") + raise TypeError("M must be a matrix") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) @@ -19975,7 +19973,7 @@ cdef class Matrix(Matrix1): elif isinstance(shifts, (FreeModuleElement, list, tuple)): shifts = (ZZ**E.nrows())(shifts) else: - raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}") if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) @@ -19984,7 +19982,7 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"degrees must not contain a negative bound.") + raise ValueError(f"degrees must not contain a negative bound") if algorithm is None: if E.base_ring().cardinality() == 2: @@ -19992,7 +19990,7 @@ cdef class Matrix(Matrix1): algorithm = 'naive' else: algorithm = 'elimination' - elif E.base_ring().is_prime_field(): + elif E.base_ring().is_prime_field() and E.base_ring().is_finite(): if E.nrows() <= 4: algorithm = 'naive' else: @@ -20007,7 +20005,7 @@ cdef class Matrix(Matrix1): elif algorithm == 'elimination': return E._krylov_basis_elimination(M, shifts, degrees, output_rows) else: - raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\".") + raise ValueError("algorithm must be one of None, \"naive\" or \"elimination\"") def krylov_kernel_basis(self, M, shifts=None, degrees=None, output_rows=True, var=None, basis_algorithm=None): r""" @@ -20077,25 +20075,25 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers (optional): row - priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers - (optional). The entry ``degrees[i]`` indicates that ``self[i, :] * - M**degrees[i]`` is known to be dependent on rows before it in the - ``shifts``-ordered Krylov matrix. If ``None``, defaults to - ``self.ncols()`` for all rows. Giving a single integer for - ``degrees`` is equivalent to giving a list with this integer repeated - ``self.nrows()`` times. - - ``output_rows`` (default: ``True``) -- boolean, optional. Determines + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers: + row priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` (default: ``None``) -- an integer or a list of + ``self.nrows()`` integers. The entry ``degrees[i]`` indicates that + ``self[i, :] * M**degrees[i]`` is known to be dependent on rows + before it in the ``shifts``-ordered Krylov matrix. If ``None``, + defaults to ``self.ncols()`` for all rows. Giving a single integer + for ``degrees`` is equivalent to giving a list with this integer + repeated ``self.nrows()`` times. + - ``output_rows`` (default: ``True``) -- boolean. Determines whether information relating the output columns to the rows of the corresponding Krylov matrix is also provided. - - ``var`` (default: ``None``) -- (optional) If ``None``, this method returns a - constant matrix. If not ``None``, the polynomial matrix + - ``var`` (default: ``None``) -- If ``None``, this method + returns a constant matrix. If not ``None``, the polynomial matrix representation is used, and ``var`` must be either the generator of a polynomial ring over the base ring of ``self``, or a string which will be used as variable name for building such a polynomial ring. - - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let - Sage choose). + - ``algorithm`` (default: ``None``) -- either ``'naive'``, + ``'elimination'``, or ``None`` (let Sage choose). OUTPUT: @@ -20280,13 +20278,13 @@ cdef class Matrix(Matrix1): TESTS: - The base ring of `self` must be an exact field:: + The method is currently only implemented over exact fields:: sage: E = matrix(RR, [[1.5, 1.2, 1.0], [5.6, 7.9, 2.3]]) sage: E.krylov_kernel_basis(E) Traceback (most recent call last): ... - NotImplementedError: self.base_ring() must be an exact field. + NotImplementedError: computing kernels of Krylov matrices is currently only implemented over exact fields The input `M` must be a matrix:: @@ -20295,22 +20293,22 @@ cdef class Matrix(Matrix1): sage: E.krylov_kernel_basis(20) Traceback (most recent call last): ... - TypeError: M must be a matrix. + TypeError: M must be a matrix The matrix `M` must be square and match `E.ncols()`:: sage: E.krylov_kernel_basis(matrix.zero(R, 3, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_kernel_basis(matrix.zero(R, 4, 3)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_kernel_basis(matrix.zero(R, 4, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_kernel_basis(matrix.zero(R, 3, 3)) ( [ 0 0 0 1 0] @@ -20377,7 +20375,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_kernel_basis(M, degrees=[2, 3, -1]) Traceback (most recent call last): ... - ValueError: degrees must not contain a negative bound. + ValueError: degrees must not contain a negative bound The algorithm must be valid:: @@ -20385,7 +20383,7 @@ cdef class Matrix(Matrix1): sage: E.krylov_kernel_basis(M, basis_algorithm="non-existent") Traceback (most recent call last): ... - ValueError: algorithm must be one of None, "naive" or "elimination". + ValueError: algorithm must be one of None, "naive" or "elimination" """ from sage.combinat.permutation import Permutation from sage.matrix.constructor import matrix @@ -20396,12 +20394,12 @@ cdef class Matrix(Matrix1): # INPUT VALIDATION if not (E.base_ring() in _Fields and E.base_ring().is_exact()): - raise NotImplementedError("self.base_ring() must be an exact field.") + raise NotImplementedError("computing kernels of Krylov matrices is currently only implemented over exact fields") if not isinstance(M, Matrix): raise TypeError("M must be a matrix.") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): - raise ValueError("M does not have correct dimensions.") + raise ValueError("M does not have correct dimensions") if M.base_ring() != E.base_ring(): E, M = coercion_model.canonical_coercion(E, M) @@ -20412,16 +20410,16 @@ cdef class Matrix(Matrix1): except AttributeError: pass if not generator: - raise TypeError('polynomial variable must be a string or polynomial ring generator, not {0}.'.format(var)) + raise TypeError('polynomial variable must be a string or polynomial ring generator, not {0}'.format(var)) elif var.base_ring() != E.base_ring(): - raise TypeError('polynomial generator must be over the same ring as the matrix entries.') + raise TypeError('polynomial generator must be over the same ring as the matrix entries') if shifts is None: shifts = (ZZ**E.nrows()).zero() elif isinstance(shifts, (FreeModuleElement, list, tuple)): shifts = (ZZ**E.nrows())(shifts) else: - raise ValueError(f"shifts must be an integer vector of length {E.nrows()}.") + raise ValueError(f"shifts must be an integer vector of length {E.nrows()}") if degrees is None: degrees = vector(ZZ, [E.ncols()] * E.nrows()) @@ -20430,7 +20428,7 @@ cdef class Matrix(Matrix1): else: degrees = (ZZ**E.nrows())([degrees] * E.nrows()) if E.nrows() > 0 and min(degrees) < 0: - raise ValueError(f"degrees must not contain a negative bound.") + raise ValueError(f"degrees must not contain a negative bound") m = E.nrows() sigma = E.ncols() From 94fc36a1cced58f96c0c48a6fe2af8fa0453ca59 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 5 Sep 2025 10:13:46 +0200 Subject: [PATCH 54/57] add spaces --- src/sage/matrix/matrix2.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index ba2c23dd3c4..dfc9a874f5b 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19644,7 +19644,7 @@ cdef class Matrix(Matrix1): M_L = M_L * M_L rows = [i for i, x in enumerate(row_profile_self) if x[1] + L <= degrees[x[0]]] - S = R.matrix_from_rows(rows)*M_L + S = R.matrix_from_rows(rows) * M_L R = matrix.block([[exhausted], [R], [S]], subdivide=False) # sort rows of R, find profile, translate to k (indices of full krylov matrix) From fae74c30100d9ab28ca2c507cfa954c56f95f6f0 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 5 Sep 2025 10:19:52 +0200 Subject: [PATCH 55/57] limit to 80 characters --- src/sage/matrix/matrix2.pyx | 49 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index dfc9a874f5b..dfbf907798c 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19137,9 +19137,9 @@ cdef class Matrix(Matrix1): the priority given to rows. - ``degrees`` -- an integer vector of length ``self.nrows()`` defining the maximum degree for rows. - - ``row_pairs`` (default: ``None``) -- the list of pairs of row indices and associated - degrees to be sorted. If ``None``, the default is taken as - `[(i, j), 0 \le i < m, 0 \le j \le d_i]` where `m` is + - ``row_pairs`` (default: ``None``) -- the list of pairs of row indices + and associated degrees to be sorted. If ``None``, the default is + taken as `[(i, j), 0 \le i < m, 0 \le j \le d_i]` where `m` is ``self.nrows()`` and `d_i` is ``degrees[i]``. OUTPUT: @@ -19248,14 +19248,15 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers, row - priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers. The entry ``degrees[i]`` indicates the number of Krylov - iterates to appear in the output (that is, ``self[i, :] * M**j`` will - appear for `j` up to ``degrees[i]``, included). If ``None``, defaults - to ``self.ncols()`` for all rows. Giving a single integer for - ``degrees`` is equivalent to giving a list with this integer repeated - ``self.nrows()`` times. + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers, + row priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` (default: ``None``) -- an integer or a list of + ``self.nrows()`` integers. The entry ``degrees[i]`` indicates the + number of Krylov iterates to appear in the output (that is, + ``self[i, :] * M**j`` will appear for `j` up to ``degrees[i]``, + included). If ``None``, defaults to ``self.ncols()`` for all rows. + Giving a single integer for ``degrees`` is equivalent to giving a + list with this integer repeated ``self.nrows()`` times. OUTPUT: @@ -19731,19 +19732,20 @@ cdef class Matrix(Matrix1): - ``M`` -- a square matrix of size equal to the number of columns of ``self``. - - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers: row - priority shifts. If ``None``, defaults to all zeroes. - - ``degrees`` (default: ``None``) -- an integer or a list of ``self.nrows()`` integers. The entry ``degrees[i]`` indicates that ``self[i, :] * - M**degrees[i]`` is known to be dependent on rows before it in the - ``shifts``-ordered Krylov matrix. If ``None``, defaults to - ``self.ncols()`` for all rows. Giving a single integer for - ``degrees`` is equivalent to giving a list with this integer repeated - ``self.nrows()`` times. + - ``shifts`` (default: ``None``) -- list of ``self.nrows()`` integers: + row priority shifts. If ``None``, defaults to all zeroes. + - ``degrees`` (default: ``None``) -- an integer or a list of + ``self.nrows()`` integers. The entry ``degrees[i]`` indicates that + ``self[i, :] * M**degrees[i]`` is known to be dependent on rows + before it in the ``shifts``-ordered Krylov matrix. If ``None``, + defaults to ``self.ncols()`` for all rows. Giving a single integer + for ``degrees`` is equivalent to giving a list with this integer + repeated ``self.nrows()`` times. - ``output_rows`` (default: ``True``) -- boolean. Determines whether information relating the output rows to their position in the Krylov matrix is also provided. - - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let - Sage choose). + - ``algorithm`` (default: ``None``) -- either ``'naive'``, + ``'elimination'``, or ``None`` (let Sage choose). OUTPUT: @@ -20012,8 +20014,9 @@ cdef class Matrix(Matrix1): Return a basis in canonical form for the kernel of the Krylov matrix of ``(self, M)`` with rows ordered according to ``shifts``. In other terms, the rows of the returned matrix form a basis of the kernel of the - `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to \Bold{K}^n` given by the matrix - ``self``, where the action of `x` on `\Bold{K}^n` is given by `M`. + `\Bold{K}[x]`-linear map `\Bold{K}[x]^n \to \Bold{K}^n` given by the + matrix ``self``, where the action of `x` on `\Bold{K}^n` is given by + `M`. Write `E` for ``self``, of dimensions `m \times n`. Consider the Krylov basis `B` as computed by :meth:`krylov_basis` with the same parameters From 8367783c9849713272d0b2749e760cea489f3be5 Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 5 Sep 2025 10:55:10 +0200 Subject: [PATCH 56/57] fix short line, bad indentation --- src/sage/matrix/matrix2.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index dfbf907798c..f11a3958e53 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19741,9 +19741,9 @@ cdef class Matrix(Matrix1): defaults to ``self.ncols()`` for all rows. Giving a single integer for ``degrees`` is equivalent to giving a list with this integer repeated ``self.nrows()`` times. - - ``output_rows`` (default: ``True``) -- boolean. Determines - whether information relating the output rows to their position in the - Krylov matrix is also provided. + - ``output_rows`` (default: ``True``) -- boolean. Determines whether + information relating the output rows to their position in the Krylov + matrix is also provided. - ``algorithm`` (default: ``None``) -- either ``'naive'``, ``'elimination'``, or ``None`` (let Sage choose). @@ -20096,7 +20096,7 @@ cdef class Matrix(Matrix1): a polynomial ring over the base ring of ``self``, or a string which will be used as variable name for building such a polynomial ring. - ``algorithm`` (default: ``None``) -- either ``'naive'``, - ``'elimination'``, or ``None`` (let Sage choose). + ``'elimination'``, or ``None`` (let Sage choose). OUTPUT: From 4ad72a653b5a998820008d365b7ff5f5673ce4df Mon Sep 17 00:00:00 2001 From: Nicholas Bell Date: Fri, 5 Sep 2025 11:12:38 +0200 Subject: [PATCH 57/57] fix doc tests --- src/sage/matrix/matrix2.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index f11a3958e53..9b6b6ff0541 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19874,15 +19874,15 @@ cdef class Matrix(Matrix1): sage: E.krylov_basis(matrix.zero(R, 3, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_basis(matrix.zero(R, 4, 3)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_basis(matrix.zero(R, 4, 4)) Traceback (most recent call last): ... - ValueError: M does not have correct dimensions. + ValueError: M does not have correct dimensions sage: E.krylov_basis(matrix.zero(R, 3, 3)) ( [27 49 29] @@ -20400,7 +20400,7 @@ cdef class Matrix(Matrix1): raise NotImplementedError("computing kernels of Krylov matrices is currently only implemented over exact fields") if not isinstance(M, Matrix): - raise TypeError("M must be a matrix.") + raise TypeError("M must be a matrix") if M.nrows() != E.ncols() or M.ncols() != E.ncols(): raise ValueError("M does not have correct dimensions") if M.base_ring() != E.base_ring():