diff --git a/src/sage/matrix/matrix2.pyx b/src/sage/matrix/matrix2.pyx index e5f2ebb453f..7485ded7e47 100644 --- a/src/sage/matrix/matrix2.pyx +++ b/src/sage/matrix/matrix2.pyx @@ -19111,6 +19111,1084 @@ cdef class Matrix(Matrix1): U.rescale_col(n - 1, -1) return U + 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)] + """ + # (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 + ``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] + [ ] + + 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 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: + + - A matrix with block rows [E, EM, EM^2, ..., EM^d], row-permuted by + ``shifts``. + + 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_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.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.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] + 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 + 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") + 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() != E.base_ring(): + E, M = coercion_model.canonical_coercion(E, M) + + if degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) + 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()}.") + + m = E.nrows() + + # store 2 blocks for the Krylov matrix + blocks = [E] + + # 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(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] <= 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] + + 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""" + 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) + 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') + ( + [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') + ( + [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') + ( + [50 58 0] + [ 0 50 58] + [ 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) + + # calculate first independent rows of K + row_profile = K.pivot_rows() + + # construct submatrix + pivot = K.matrix_from_rows(row_profile) + + if not output_rows: + return pivot + + 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 + + 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. + + 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.matrix.constructor import matrix + import math + from sage.combinat.permutation import Permutation + + m = self.nrows() + sigma = self.ncols() + + if m == 0: + 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_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_rows[i], 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([]), ()) 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(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 <= degrees[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 (shifts[c]+d, c) + 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: + M_L = M + else: + 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) + 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) + + 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: + 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] + imi = [i for i in row_profile_R if k[k_rows[i]][0] not in excluded_rows] + + 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) + else: + 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_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: + R = exhausted + row_profile_self = row_profile_exhausted + elif exhausted.nrows() != 0: + k = row_profile_exhausted + row_profile_self + R = exhausted.stack(R) + + 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_rows[i]] for i in range(len(k))] + + 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), 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, 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``. + + .. 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. + + 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. + - ``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 + 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 + + E = self + + # 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") + + if not isinstance(M, Matrix): + raise TypeError("krylov_basis: M is not a matrix") + 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() != E.base_ring(): + E, M = coercion_model.canonical_coercion(E, M) + + if degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) + 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()}.") + + if algorithm is None: + if E.base_ring().order() == 2: + if E.nrows() <= 12: + algorithm = 'naive' + else: + algorithm = 'elimination' + elif E.base_ring().degree() == 1: + if E.nrows() <= 4: + algorithm = 'naive' + else: + algorithm = 'elimination' + else: + if E.nrows() <= 2: + algorithm = 'naive' + else: + algorithm = 'elimination' + if algorithm == 'naive': + return E._krylov_basis_naive(M, shifts, degrees, output_rows) + 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\".") + + 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())`` + + 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') + [x^2 + 40*x + 82 76 0] + [ 3*x + 13 x + 57 0] + [ 96 96 1] + 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: 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() + 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)[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 + 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() + 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)[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 + 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() + 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)[1]) == sum(basis[i,i].degree() for i in range(E.nrows())) + True + """ + 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 + + E = self + + # 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(): + raise ValueError("krylov_kernel_basis: matrix M does not have correct dimensions.") + 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 degrees is None: + degrees = vector(ZZ, [E.ncols()] * E.nrows()) + elif isinstance(degrees, (FreeModuleElement, list, tuple)): + degrees = (ZZ**E.nrows())(degrees) + 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()}.") + + m = E.nrows() + sigma = E.ncols() + base_ring = E.base_ring() + + # 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) + + 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 + 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 = 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 + 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 = krylov_basis.matrix_from_columns(col_profile) + D = matrix(D_rows) + 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 + else: + poly_ring = PolynomialRing(base_ring, var) + + # construct coefficient map + coeffs_map = [[{} for _ in range(m)] for _ in range(m)] + # add identity part of kernel basis + for i in range(m): + 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], 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 def T(self):