rref.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. # Algorithms for computing the reduced row echelon form of a matrix.
  2. #
  3. # We need to choose carefully which algorithms to use depending on the domain,
  4. # shape, and sparsity of the matrix as well as things like the bit count in the
  5. # case of ZZ or QQ. This is important because the algorithms have different
  6. # performance characteristics in the extremes of dense vs sparse.
  7. #
  8. # In all cases we use the sparse implementations but we need to choose between
  9. # Gauss-Jordan elimination with division and fraction-free Gauss-Jordan
  10. # elimination. For very sparse matrices over ZZ with low bit counts it is
  11. # asymptotically faster to use Gauss-Jordan elimination with division. For
  12. # dense matrices with high bit counts it is asymptotically faster to use
  13. # fraction-free Gauss-Jordan.
  14. #
  15. # The most important thing is to get the extreme cases right because it can
  16. # make a big difference. In between the extremes though we have to make a
  17. # choice and here we use empirically determined thresholds based on timings
  18. # with random sparse matrices.
  19. #
  20. # In the case of QQ we have to consider the denominators as well. If the
  21. # denominators are small then it is faster to clear them and use fraction-free
  22. # Gauss-Jordan over ZZ. If the denominators are large then it is faster to use
  23. # Gauss-Jordan elimination with division over QQ.
  24. #
  25. # Timings for the various algorithms can be found at
  26. #
  27. # https://github.com/sympy/sympy/issues/25410
  28. # https://github.com/sympy/sympy/pull/25443
  29. from sympy.polys.domains import ZZ
  30. from sympy.polys.matrices.sdm import SDM, sdm_irref, sdm_rref_den
  31. from sympy.polys.matrices.ddm import DDM
  32. from sympy.polys.matrices.dense import ddm_irref, ddm_irref_den
  33. def _dm_rref(M, *, method='auto'):
  34. """
  35. Compute the reduced row echelon form of a ``DomainMatrix``.
  36. This function is the implementation of :meth:`DomainMatrix.rref`.
  37. Chooses the best algorithm depending on the domain, shape, and sparsity of
  38. the matrix as well as things like the bit count in the case of :ref:`ZZ` or
  39. :ref:`QQ`. The result is returned over the field associated with the domain
  40. of the Matrix.
  41. See Also
  42. ========
  43. sympy.polys.matrices.domainmatrix.DomainMatrix.rref
  44. The ``DomainMatrix`` method that calls this function.
  45. sympy.polys.matrices.rref._dm_rref_den
  46. Alternative function for computing RREF with denominator.
  47. """
  48. method, use_fmt = _dm_rref_choose_method(M, method, denominator=False)
  49. M, old_fmt = _dm_to_fmt(M, use_fmt)
  50. if method == 'GJ':
  51. # Use Gauss-Jordan with division over the associated field.
  52. Mf = _to_field(M)
  53. M_rref, pivots = _dm_rref_GJ(Mf)
  54. elif method == 'FF':
  55. # Use fraction-free GJ over the current domain.
  56. M_rref_f, den, pivots = _dm_rref_den_FF(M)
  57. M_rref = _to_field(M_rref_f) / den
  58. elif method == 'CD':
  59. # Clear denominators and use fraction-free GJ in the associated ring.
  60. _, Mr = M.clear_denoms_rowwise(convert=True)
  61. M_rref_f, den, pivots = _dm_rref_den_FF(Mr)
  62. M_rref = _to_field(M_rref_f) / den
  63. else:
  64. raise ValueError(f"Unknown method for rref: {method}")
  65. M_rref, _ = _dm_to_fmt(M_rref, old_fmt)
  66. # Invariants:
  67. # - M_rref is in the same format (sparse or dense) as the input matrix.
  68. # - M_rref is in the associated field domain and any denominator was
  69. # divided in (so is implicitly 1 now).
  70. return M_rref, pivots
  71. def _dm_rref_den(M, *, keep_domain=True, method='auto'):
  72. """
  73. Compute the reduced row echelon form of a ``DomainMatrix`` with denominator.
  74. This function is the implementation of :meth:`DomainMatrix.rref_den`.
  75. Chooses the best algorithm depending on the domain, shape, and sparsity of
  76. the matrix as well as things like the bit count in the case of :ref:`ZZ` or
  77. :ref:`QQ`. The result is returned over the same domain as the input matrix
  78. unless ``keep_domain=False`` in which case the result might be over an
  79. associated ring or field domain.
  80. See Also
  81. ========
  82. sympy.polys.matrices.domainmatrix.DomainMatrix.rref_den
  83. The ``DomainMatrix`` method that calls this function.
  84. sympy.polys.matrices.rref._dm_rref
  85. Alternative function for computing RREF without denominator.
  86. """
  87. method, use_fmt = _dm_rref_choose_method(M, method, denominator=True)
  88. M, old_fmt = _dm_to_fmt(M, use_fmt)
  89. if method == 'FF':
  90. # Use fraction-free GJ over the current domain.
  91. M_rref, den, pivots = _dm_rref_den_FF(M)
  92. elif method == 'GJ':
  93. # Use Gauss-Jordan with division over the associated field.
  94. M_rref_f, pivots = _dm_rref_GJ(_to_field(M))
  95. # Convert back to the ring?
  96. if keep_domain and M_rref_f.domain != M.domain:
  97. _, M_rref = M_rref_f.clear_denoms(convert=True)
  98. if pivots:
  99. den = M_rref[0, pivots[0]].element
  100. else:
  101. den = M_rref.domain.one
  102. else:
  103. # Possibly an associated field
  104. M_rref = M_rref_f
  105. den = M_rref.domain.one
  106. elif method == 'CD':
  107. # Clear denominators and use fraction-free GJ in the associated ring.
  108. _, Mr = M.clear_denoms_rowwise(convert=True)
  109. M_rref_r, den, pivots = _dm_rref_den_FF(Mr)
  110. if keep_domain and M_rref_r.domain != M.domain:
  111. # Convert back to the field
  112. M_rref = _to_field(M_rref_r) / den
  113. den = M.domain.one
  114. else:
  115. # Possibly an associated ring
  116. M_rref = M_rref_r
  117. if pivots:
  118. den = M_rref[0, pivots[0]].element
  119. else:
  120. den = M_rref.domain.one
  121. else:
  122. raise ValueError(f"Unknown method for rref: {method}")
  123. M_rref, _ = _dm_to_fmt(M_rref, old_fmt)
  124. # Invariants:
  125. # - M_rref is in the same format (sparse or dense) as the input matrix.
  126. # - If keep_domain=True then M_rref and den are in the same domain as the
  127. # input matrix
  128. # - If keep_domain=False then M_rref might be in an associated ring or
  129. # field domain but den is always in the same domain as M_rref.
  130. return M_rref, den, pivots
  131. def _dm_to_fmt(M, fmt):
  132. """Convert a matrix to the given format and return the old format."""
  133. old_fmt = M.rep.fmt
  134. if old_fmt == fmt:
  135. pass
  136. elif fmt == 'dense':
  137. M = M.to_dense()
  138. elif fmt == 'sparse':
  139. M = M.to_sparse()
  140. else:
  141. raise ValueError(f'Unknown format: {fmt}') # pragma: no cover
  142. return M, old_fmt
  143. # These are the four basic implementations that we want to choose between:
  144. def _dm_rref_GJ(M):
  145. """Compute RREF using Gauss-Jordan elimination with division."""
  146. if M.rep.fmt == 'sparse':
  147. return _dm_rref_GJ_sparse(M)
  148. else:
  149. return _dm_rref_GJ_dense(M)
  150. def _dm_rref_den_FF(M):
  151. """Compute RREF using fraction-free Gauss-Jordan elimination."""
  152. if M.rep.fmt == 'sparse':
  153. return _dm_rref_den_FF_sparse(M)
  154. else:
  155. return _dm_rref_den_FF_dense(M)
  156. def _dm_rref_GJ_sparse(M):
  157. """Compute RREF using sparse Gauss-Jordan elimination with division."""
  158. M_rref_d, pivots, _ = sdm_irref(M.rep)
  159. M_rref_sdm = SDM(M_rref_d, M.shape, M.domain)
  160. pivots = tuple(pivots)
  161. return M.from_rep(M_rref_sdm), pivots
  162. def _dm_rref_GJ_dense(M):
  163. """Compute RREF using dense Gauss-Jordan elimination with division."""
  164. partial_pivot = M.domain.is_RR or M.domain.is_CC
  165. ddm = M.rep.to_ddm().copy()
  166. pivots = ddm_irref(ddm, _partial_pivot=partial_pivot)
  167. M_rref_ddm = DDM(ddm, M.shape, M.domain)
  168. pivots = tuple(pivots)
  169. return M.from_rep(M_rref_ddm.to_dfm_or_ddm()), pivots
  170. def _dm_rref_den_FF_sparse(M):
  171. """Compute RREF using sparse fraction-free Gauss-Jordan elimination."""
  172. M_rref_d, den, pivots = sdm_rref_den(M.rep, M.domain)
  173. M_rref_sdm = SDM(M_rref_d, M.shape, M.domain)
  174. pivots = tuple(pivots)
  175. return M.from_rep(M_rref_sdm), den, pivots
  176. def _dm_rref_den_FF_dense(M):
  177. """Compute RREF using sparse fraction-free Gauss-Jordan elimination."""
  178. ddm = M.rep.to_ddm().copy()
  179. den, pivots = ddm_irref_den(ddm, M.domain)
  180. M_rref_ddm = DDM(ddm, M.shape, M.domain)
  181. pivots = tuple(pivots)
  182. return M.from_rep(M_rref_ddm.to_dfm_or_ddm()), den, pivots
  183. def _dm_rref_choose_method(M, method, *, denominator=False):
  184. """Choose the fastest method for computing RREF for M."""
  185. if method != 'auto':
  186. if method.endswith('_dense'):
  187. method = method[:-len('_dense')]
  188. use_fmt = 'dense'
  189. else:
  190. use_fmt = 'sparse'
  191. else:
  192. # The sparse implementations are always faster
  193. use_fmt = 'sparse'
  194. K = M.domain
  195. if K.is_ZZ:
  196. method = _dm_rref_choose_method_ZZ(M, denominator=denominator)
  197. elif K.is_QQ:
  198. method = _dm_rref_choose_method_QQ(M, denominator=denominator)
  199. elif K.is_RR or K.is_CC:
  200. # TODO: Add partial pivot support to the sparse implementations.
  201. method = 'GJ'
  202. use_fmt = 'dense'
  203. elif K.is_EX and M.rep.fmt == 'dense' and not denominator:
  204. # Do not switch to the sparse implementation for EX because the
  205. # domain does not have proper canonicalization and the sparse
  206. # implementation gives equivalent but non-identical results over EX
  207. # from performing arithmetic in a different order. Specifically
  208. # test_issue_23718 ends up getting a more complicated expression
  209. # when using the sparse implementation. Probably the best fix for
  210. # this is something else but for now we stick with the dense
  211. # implementation for EX if the matrix is already dense.
  212. method = 'GJ'
  213. use_fmt = 'dense'
  214. else:
  215. # This is definitely suboptimal. More work is needed to determine
  216. # the best method for computing RREF over different domains.
  217. if denominator:
  218. method = 'FF'
  219. else:
  220. method = 'GJ'
  221. return method, use_fmt
  222. def _dm_rref_choose_method_QQ(M, *, denominator=False):
  223. """Choose the fastest method for computing RREF over QQ."""
  224. # The same sorts of considerations apply here as in the case of ZZ. Here
  225. # though a new more significant consideration is what sort of denominators
  226. # we have and what to do with them so we focus on that.
  227. # First compute the density. This is the average number of non-zero entries
  228. # per row but only counting rows that have at least one non-zero entry
  229. # since RREF can ignore fully zero rows.
  230. density, _, ncols = _dm_row_density(M)
  231. # For sparse matrices use Gauss-Jordan elimination over QQ regardless.
  232. if density < min(5, ncols/2):
  233. return 'GJ'
  234. # Compare the bit-length of the lcm of the denominators to the bit length
  235. # of the numerators.
  236. #
  237. # The threshold here is empirical: we prefer rref over QQ if clearing
  238. # denominators would result in a numerator matrix having 5x the bit size of
  239. # the current numerators.
  240. numers, denoms = _dm_QQ_numers_denoms(M)
  241. numer_bits = max([n.bit_length() for n in numers], default=1)
  242. denom_lcm = ZZ.one
  243. for d in denoms:
  244. denom_lcm = ZZ.lcm(denom_lcm, d)
  245. if denom_lcm.bit_length() > 5*numer_bits:
  246. return 'GJ'
  247. # If we get here then the matrix is dense and the lcm of the denominators
  248. # is not too large compared to the numerators. For particularly small
  249. # denominators it is fastest just to clear them and use fraction-free
  250. # Gauss-Jordan over ZZ. With very small denominators this is a little
  251. # faster than using rref_den over QQ but there is an intermediate regime
  252. # where rref_den over QQ is significantly faster. The small denominator
  253. # case is probably very common because small fractions like 1/2 or 1/3 are
  254. # often seen in user inputs.
  255. if denom_lcm.bit_length() < 50:
  256. return 'CD'
  257. else:
  258. return 'FF'
  259. def _dm_rref_choose_method_ZZ(M, *, denominator=False):
  260. """Choose the fastest method for computing RREF over ZZ."""
  261. # In the extreme of very sparse matrices and low bit counts it is faster to
  262. # use Gauss-Jordan elimination over QQ rather than fraction-free
  263. # Gauss-Jordan over ZZ. In the opposite extreme of dense matrices and high
  264. # bit counts it is faster to use fraction-free Gauss-Jordan over ZZ. These
  265. # two extreme cases need to be handled differently because they lead to
  266. # different asymptotic complexities. In between these two extremes we need
  267. # a threshold for deciding which method to use. This threshold is
  268. # determined empirically by timing the two methods with random matrices.
  269. # The disadvantage of using empirical timings is that future optimisations
  270. # might change the relative speeds so this can easily become out of date.
  271. # The main thing is to get the asymptotic complexity right for the extreme
  272. # cases though so the precise value of the threshold is hopefully not too
  273. # important.
  274. # Empirically determined parameter.
  275. PARAM = 10000
  276. # First compute the density. This is the average number of non-zero entries
  277. # per row but only counting rows that have at least one non-zero entry
  278. # since RREF can ignore fully zero rows.
  279. density, nrows_nz, ncols = _dm_row_density(M)
  280. # For small matrices use QQ if more than half the entries are zero.
  281. if nrows_nz < 10:
  282. if density < ncols/2:
  283. return 'GJ'
  284. else:
  285. return 'FF'
  286. # These are just shortcuts for the formula below.
  287. if density < 5:
  288. return 'GJ'
  289. elif density > 5 + PARAM/nrows_nz:
  290. return 'FF' # pragma: no cover
  291. # Maximum bitsize of any entry.
  292. elements = _dm_elements(M)
  293. bits = max([e.bit_length() for e in elements], default=1)
  294. # Wideness parameter. This is 1 for square or tall matrices but >1 for wide
  295. # matrices.
  296. wideness = max(1, 2/3*ncols/nrows_nz)
  297. max_density = (5 + PARAM/(nrows_nz*bits**2)) * wideness
  298. if density < max_density:
  299. return 'GJ'
  300. else:
  301. return 'FF'
  302. def _dm_row_density(M):
  303. """Density measure for sparse matrices.
  304. Defines the "density", ``d`` as the average number of non-zero entries per
  305. row except ignoring rows that are fully zero. RREF can ignore fully zero
  306. rows so they are excluded. By definition ``d >= 1`` except that we define
  307. ``d = 0`` for the zero matrix.
  308. Returns ``(density, nrows_nz, ncols)`` where ``nrows_nz`` counts the number
  309. of nonzero rows and ``ncols`` is the number of columns.
  310. """
  311. # Uses the SDM dict-of-dicts representation.
  312. ncols = M.shape[1]
  313. rows_nz = M.rep.to_sdm().values()
  314. if not rows_nz:
  315. return 0, 0, ncols
  316. else:
  317. nrows_nz = len(rows_nz)
  318. density = sum(map(len, rows_nz)) / nrows_nz
  319. return density, nrows_nz, ncols
  320. def _dm_elements(M):
  321. """Return nonzero elements of a DomainMatrix."""
  322. elements, _ = M.to_flat_nz()
  323. return elements
  324. def _dm_QQ_numers_denoms(Mq):
  325. """Returns the numerators and denominators of a DomainMatrix over QQ."""
  326. elements = _dm_elements(Mq)
  327. numers = [e.numerator for e in elements]
  328. denoms = [e.denominator for e in elements]
  329. return numers, denoms
  330. def _to_field(M):
  331. """Convert a DomainMatrix to a field if possible."""
  332. K = M.domain
  333. if K.has_assoc_Field:
  334. return M.to_field()
  335. else:
  336. return M